useState は、コンポーネントがレンダリングの間で何かを覚えておけるようにするフックです。カウント、入力欄のテキスト、メニューが開いているかどうか、などです。セッターを呼ぶたびに、Reactは新しい値でコンポーネントをもう一度レンダリングします。
ボタンをクリックしてから、エディタで useState(0) を useState(10) に変えてみてください。プレビューが再構築され、カウントは10から始まります。
構文
const [value, setValue] = useState(initialValue);
initialValueは最初のレンダリングでだけ使われます。それ以降のレンダリングでは、Reactはこれを無視して保存している値を返します。valueはこのレンダリングでの現在の値です。setValueは値を置き換え、新しいレンダリングを予約します。
名前は自由に付けられます。useState は要素が2つの配列を返し、角かっこがそれを取り出します。つまり [isOpen, setIsOpen] も [email, setEmail] も同じパターンです。
useState はコンポーネントのトップレベルで呼び出し、ループ、条件分岐、ネストした関数の中では呼ばないでください。Reactは呼び出しの順番で各呼び出しと保存した値を対応させるため、その順番はすべてのレンダリングで同じでなければなりません(フックのルールを参照)。
複数のstate
コンポーネントは必要なだけ useState を呼び出せます。呼び出しごとに独立した値になります。
stateはスナップショット
セッターは、手元にある変数を変えません。次のレンダリングで値がどうなるべきかをReactに伝えるだけです。そのレンダリングが起きるまで、現在のレンダリングは古い値を見続けます。
プレビューの下のコンソールを開き、1回クリックしてください。ハンドラは古い値の 0 を出力し、その後コンポーネントが 1 での新しいレンダリングを出力します。何も間違っていません。count はレンダリングの間ずっと定数で、次のレンダリングが新しい値を受け取るのです。
更新関数
スナップショットなので、count + 1 でセッターを3回呼んでも、増えるのは3ではなく1です。どの呼び出しも 0 + 1 を計算するからです。代わりに関数を渡すと、Reactは各呼び出しに前の呼び出しの結果を渡します。
次の値が前の値に依存し、その更新が最新のレンダリングから直接実行されるとは限らない場合は、更新関数の形を使ってください。1つのハンドラで複数回セットする場合、setInterval のコールバック、エフェクトなどです。
オブジェクトと配列のstate
Reactは古い値と新しい値を Object.is で比べて、もう一度レンダリングするかを決めます。オブジェクトをその場で変更して同じオブジェクトを渡し直すと、比較は「何も変わっていない」と判断し、画面はそのままです。必ず新しいオブジェクトや配列を渡してください。
{ ...user, age: user.age + 1 } はすべてのフィールドをコピーして1つを上書きします。[...tags, 'code'] は要素を追加し、filter は要素を取り除きます。どちらも新しい配列を返します。配列とオブジェクトの更新のページでは、ネストしたオブジェクト、リスト内の1項目の編集、途中への挿入を扱います。
遅延初期化
useState の引数は、Reactが最初の1回しか使わないにもかかわらず、レンダリングのたびに評価されます。計算が重い場合(localStorage の読み込み、大きな文字列のパースなど)は関数を渡してください。Reactは最初のレンダリングで1回だけそれを呼びます。
// Runs readSavedNotes() on every render, then throws the result away
const [notes, setNotes] = useState(readSavedNotes());
// Runs it once
const [notes, setNotes] = useState(readSavedNotes);
const [notes, setNotes] = useState(() => readSavedNotes());
keyでstateをリセットする
stateはツリー内のコンポーネントの位置に属しています。同じ場所に別の key をレンダリングすると、Reactはそれを新しいコンポーネントとして扱い、stateは最初からになります。編集対象が変わったときにフォームをリセットする、最もすっきりした方法です。
メッセージを入力してから宛先を切り替えると、下書きが消えます。key={to} を削除してもう一度試すと、テキストは残ります。Reactが同じ場所に同じ Draft があると見なすからです。
よくある間違い
セットした直後にstateをログに出す。上で見たとおり、古い値が表示されます。コンポーネント本体でログを出すか、先に次の値を変数に計算してそれを使ってください。
stateを直接変更する。user.age++; setUser(user) は同じオブジェクトを渡すので、何もレンダリングされません。先にコピーしてください。
レンダリング中にセッターを呼ぶ。<button onClick={setCount(count + 1)}> はレンダリング中に setCount を呼び、それがもう一度レンダリングを起こし、またそれを呼びます。Reactはこのループを「Too many re-renders」で止めます。関数を渡してください:onClick={() => setCount(count + 1)}。
計算できる値を保存する。fullName が常に first + ' ' + last なら、ずれる可能性のある3つ目のstateを持たず、レンダリング中に計算してください。
よくある質問
useStateは何を返しますか?
要素がちょうど2つの配列です。現在の値と、それを置き換える関数です。名前は分割代入で自分で付けます。慣習では [thing, setThing] です。
stateがすぐに更新されないのはなぜですか?
セッターを呼んでも、手元にある変数は変わりません。Reactにコンポーネントをもう一度レンダリングするよう頼むだけで、新しい値はその次のレンダリングで現れます。現在のイベントハンドラの中では古い値がスコープに残っているため、setCount(count + 1) の直後の console.log(count) は古い数値を出力します。
セッターに関数を渡すのはどんなときですか?
新しい値が古い値に依存していて、それを複数回セットするとき、またはタイマーやエフェクトの中からセットするときです。setCount(c => c + 1) は常に最新の値から始まり、setCount(count + 1) はこのレンダリングが見ていた値から始まります。
useStateにオブジェクトや配列を入れられますか?
はい。ただし読み取り専用として扱います。新しいオブジェクトや配列を作ってセッターに渡してください(setUser({ ...user, name })、setItems([...items, item]))。既存のものをその場で変更しても、レンダリングは起きません。
useStateとuseReducerの違いは何ですか?
どちらもstateを保存します。useStateは少数の独立した値に向いています。useReducerは更新ロジックをアクションを受け取る1つの関数にまとめるので、多くのイベントが同じstateを関連した形で変えるときに読みやすくなります。