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 と併用する
useReducer と Context を組み合わせると、
ツリー深部のどのコンポーネントからでも 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点を意識しましょう。
- 冗長性をなくし、矛盾しない状態を保つ
- 必要な範囲で state を共有(リフトアップ)する
- key と構造で state の保持・破棄を制御する
- 複雑なロジックは useReducer + Context で整理する
📖 参考
Discussion