useState

useState による状態管理、更新関数、オブジェクトと配列を安全に更新する考え方を学ぶ。

useState は、関数コンポーネントに「レンダーをまたいで覚えておく値」を持たせる Hook です。ユーザーの入力、開閉状態、選択中の項目のように、値の変化を画面へ反映したいときに使います。

state が必要になる場面

コンポーネント内の通常の変数は、レンダーのたびに最初から評価されます。また、変数を書き換えただけでは React は画面を更新しません。

useState は値と setter を返します。setter に次の値を渡すと、React は次のレンダーを予約します。

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button type="button" onClick={() => setCount(count + 1)}>
      {count} 回クリック
    </button>
  );
}

count はそのレンダー時点の値です。setter を呼んだ直後に同じ関数内の count が変わるわけではなく、次のレンダーで新しい値が渡されます。

前の state を基準に更新する

次の値を前の state から計算するなら、updater 関数を渡します。

setCount((current) => current + 1);

これは連続した更新で特に重要です。次の例では、それぞれの updater が直前の計算結果を受け取ります。

setCount((current) => current + 1);
setCount((current) => current + 1);
setCount((current) => current + 1);

オブジェクトと配列は置き換える

state に保存したオブジェクトや配列を直接変更しないでください。元の値を変更すると、変更前と変更後を比較できず、意図しない表示になります。

spread 構文で新しい値を作り、setter に渡します。

const [profile, setProfile] = useState({ name: 'Aki', subscribed: false });

function subscribe() {
  setProfile((current) => ({ ...current, subscribed: true }));
}

配列も同様に、map、filter、spread 構文などで新しい配列を作ります。深くネストした state は更新が難しくなるため、関連しない値を分けたり、データ構造を平坦にしたりすることも検討します。

state を設計する目安

state は「時間とともに変わり、表示に影響する最小の値」にします。既存の props や state から計算できる値は、通常は別の state に保存せず、レンダー中に計算します。

const completedCount = todos.filter((todo) => todo.done).length;

このように導出すれば、元の todos と集計値がずれる心配がありません。state を増やす前に、その値が本当に記憶する必要があるかを確認しましょう。

Hooks を呼ぶ場所

useState を含む Hooks は、関数コンポーネントまたはカスタム Hook のトップレベルで呼びます。条件分岐、ループ、イベントハンドラの中で呼ぶことはできません。React は呼び出し順で state を対応付けるため、毎回同じ順序で呼ばれる必要があります。

初期値の計算が重いとき

初期値を作る処理が重い場合、関数を initializer として渡せます。createInitialTodos() のように呼び出すのではなく、関数そのものを渡す点が重要です。

const [todos, setTodos] = useState(createInitialTodos);

これにより初期化のための計算は初回にだけ必要になります。initializer も updater と同様に純粋な関数にします。開発時の Strict Mode では、意図しない副作用を見つけるために複数回呼ばれることがあります。

state はスナップショットとして読む

イベントハンドラが実行中に見えている state は、そのレンダー時点のスナップショットです。

function handleClick() {
  setCount(count + 1);
  console.log(count); // 更新前の値
}

直後に次の値を使いたいなら、先にローカル変数として計算します。

const nextCount = count + 1;
setCount(nextCount);
sendAnalytics(nextCount);

この性質を知っていると、「setter を呼んだのに値が変わらない」ように見える場面を正しく理解できます。

state をリセットしたいとき

コンポーネントの identity が変われば state も新しくなります。たとえば編集対象の切り替えでフォーム全体を初期化したいときは、対象 ID を key にできます。

<ProfileForm key={user.id} user={user} />

key が変わると React は別のコンポーネントとして扱い、配下の state を作り直します。部分的な値だけをリセットしたい場合は、イベントハンドラで必要な state を更新する方が意図を表しやすいこともあります。

よくある落とし穴

  • レンダー中に無条件で setter を呼ぶと、再レンダーの無限ループになる
  • onClick={save()} はレンダー時に実行される。onClick={save} のように関数を渡す
  • state に関数を保存する場合は、setHandler(() => handler) のように関数を返す updater を使う

state は UI の記憶です。保存する値を最小にし、更新を純粋に保つことが、コンポーネントを予測可能にします。

Sources