6.1 stateに保存すべきもの

次の条件をすべて満たす値がstate候補になる。

  • 時間とともに変わる
  • レンダリングに影響する
  • propsや既存stateから直接計算できない

派生値はstateに保存しない。

const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");

// stateにしない
const fullName = `${lastName} ${firstName}`.trim();

6.2 冗長なstate

悪い例:

const [items, setItems] = useState<Item[]>([]);
const [selectedItem, setSelectedItem] = useState<Item | null>(null);

itemsが更新されるとselectedItemが古くなる可能性がある。

改善:

const [selectedId, setSelectedId] = useState<string | null>(null);

const selectedItem =
  items.find(item => item.id === selectedId) ?? null;

6.3 stateの正規化

入れ子が深く重複したstateは更新が難しい。

type State = {
  entities: Record<string, Task>;
  ids: string[];
  selectedId: string | null;
};

正規化すると、単一のエンティティ更新が明確になる。

6.4 stateを引き上げる

兄弟コンポーネントが同じ値を使う場合、共通の親へ移す。

function TemperatureCalculator() {
  const [celsius, setCelsius] = useState("");

  return (
    <>
      <CelsiusInput value={celsius} onChange={setCelsius} />
      <FahrenheitDisplay celsius={celsius} />
    </>
  );
}

6.5 useReducer

複数の関連更新をイベントとして表現したい場合に使う。

type State = {
  items: Task[];
};

type Action =
  | { type: "added"; task: Task }
  | { type: "toggled"; id: string }
  | { type: "removed"; id: string };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "added":
      return {
        ...state,
        items: [...state.items, action.task],
      };

    case "toggled":
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.id
            ? { ...item, completed: !item.completed }
            : item
        ),
      };

    case "removed":
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.id),
      };

    default:
      return assertNever(action);
  }
}

function assertNever(value: never): never {
  throw new Error(`未知のaction: ${JSON.stringify(value)}`);
}

Reducerは純粋でなければならない。

6.6 Context

Contextは、深い階層へ値を渡すための仕組みである。

const ThemeContext = createContext<"light" | "dark">("light");

function App() {
  return (
    <ThemeContext value="dark">
      <Page />
    </ThemeContext>
  );
}

React 19ではContext自身をProviderとして使用できる。

function Toolbar() {
  const theme = use(ThemeContext);
  return <button data-theme={theme}>操作</button>;
}

Contextはグローバルstate管理そのものではない。値が変わると利用側が再レンダリングされるため、更新頻度と責務を考える。

6.7 外部ストア

React外部の状態管理ライブラリやブラウザAPIへ接続する場合、useSyncExternalStoreを利用する。

const online = useSyncExternalStore(
  subscribe,
  () => navigator.onLine,
  () => true
);