📝

state の管理 

に公開

Reactにおけるstateの管理

Reactでアプリケーションを構築するとき、
どこで state を持つか」「どうやって複数コンポーネントで共有するか
という判断は、明快さ・保守性・拡張性を大きく左右します。

この記事では、React公式ドキュメントの考え方に沿って、state設計の基本指針を整理します。


基本方針

1. 冗長・重複をなくす

コンポーネントの props や既存の state からレンダー時に計算できる情報は、state に入れない。

例えば:

// ❌ NG: fullName を state に持っている
const [fullName, setFullName] = useState(`${firstName} ${lastName}`);

// ✅ OK: レンダリング時に計算する
const fullName = `${firstName} ${lastName}`;

2. 矛盾を避ける

ありえない組み合わせが発生する複数のブール state は、
一つの status state にまとめましょう。

// ❌ NG: 複数の boolean が矛盾する可能性
const [isTyping, setIsTyping] = useState(false);
const [isSending, setIsSending] = useState(false);

// ✅ OK: 状態を一元管理
const [status, setStatus] = useState("typing"); // "typing" | "sending" | "sent"

3. 正規化(フラット化)する

深くネストされた state は更新が難しく、重複しやすいです。
IDと辞書(マップ)で管理する正規化を意識すると、部分更新が容易になります。

// ❌ NG: ネストが深く管理が煩雑
state = {
  projects: [
    {
      id: 1,
      name: "A",
      tasks: [{ id: 101, name: "Task 1" }],
    },
  ],
};

// ✅ OK: 正規化
state = {
  projects: { 1: { id: 1, name: "A", tasks: [101] } },
  tasks: { 101: { id: 101, name: "Task 1" } },
};

4. props を state にコピーしない

親から渡された props をそのまま state にコピーすると、
後続の props 変更を反映できなくなる場合があります。

// ❌ NG: props を state にコピー
const [value, setValue] = useState(props.value);

// ✅ OK: props を直接使う or 明示的に初期値だけ使う
const value = props.value;

state のリフトアップ

複数コンポーネントで同じ情報を共有する場合は、
最も近い共通の親コンポーネントへ state を移動させます。
これを「state のリフトアップ」と呼びます。

例:パネルの展開状態を共有する

「同時に1つだけ展開されるパネル」を実現するには、親が activeIndex を管理します。

function Accordion({ items }) {
  const [activeIndex, setActiveIndex] = useState(null);

  return items.map((item, i) => (
    <Panel
      key={i}
      title={item.title}
      isActive={activeIndex === i}
      onClick={() => setActiveIndex(i)}
    />
  ));
}

制御 / 非制御の設計

  • 制御されたコンポーネント:親の props によって動作が決まる
  • 非制御コンポーネント:ローカル state で自己完結する

協調動作や一貫性を重視する場合は、「制御」側に寄せましょう。


state の保持とリセット

Reactは「ツリー内の同じ位置に同じコンポーネントがレンダーされ続ける限り state を保持」します。

位置で保持/破棄が決まる

同じ位置で異なるコンポーネントに切り替えると、
元のコンポーネントの state は破棄されます。

key によるリセット

同じ位置・同じ種類のコンポーネントでも、key を変えると別インスタンス扱いになります。

// 相手が切り替わったら入力をリセット
<Chat key={to.id} contact={to} />

意図した保持の戦略

「見えないだけ」で state を保持したいなら:

  • 非表示にしてツリーから外さない
  • または、親に state をリフトアップして保持させる

複雑な state ロジックの整理

イベントや状態が増えて setState が散在してくると、
状態遷移の把握が難しくなります。
そんなときは useReducer + Context の活用を検討しましょう。


useReducer を使う

更新ロジックを reducer に集約し、コンポーネントは
何が起こったか」を dispatch({ type, ...payload }) で伝えるだけにします。

function tasksReducer(state, action) {
  switch (action.type) {
    case "added":
      return [...state, action.task];
    case "deleted":
      return state.filter(t => t.id !== action.id);
    default:
      throw new Error("Unknown action");
  }
}

function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, []);
}

メリット:

  • アクション(意図)と更新ロジック(結果)が分離される
  • デバッグ・テストが容易
  • reducer は 純関数 として副作用を持たず、イミュータブルに更新

Context と併用する

useReducerContext を組み合わせると、
ツリー深部のどのコンポーネントからでも useContext 経由で
state を読む・更新する ことができます。

const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);

export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(tasksReducer, []);
  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        {children}
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

export function useTasks() {
  return useContext(TasksContext);
}
export function useTasksDispatch() {
  return useContext(TasksDispatchContext);
}

これにより:

  • **prop drilling(深い階層でのprops渡し)**を解消
  • Provider + カスタムフック により、状態管理のコードが整理される
  • コンポーネント側は「何を表示するか」に集中できる

まとめ

React の state 設計では、次の4点を意識しましょう。

  1. 冗長性をなくし、矛盾しない状態を保つ
  2. 必要な範囲で state を共有(リフトアップ)する
  3. key と構造で state の保持・破棄を制御する
  4. 複雑なロジックは useReducer + Context で整理する

📖 参考

Discussion