4 段階の判断フロー
① props — 親→子への一方向。1〜2 階層なら最初の選択肢
↓ 兄弟間で共有が必要になった
② state lifting — 共通の親に state を上げる
↓ ロジックを複数コンポーネントで再利用したい
③ custom hook — ロジックを切り出す(state は各コンポーネントで独立)
↓ ツリー全体に渡したい・props drilling が深すぎる
④ context — 変更頻度が低い値をツリーに広める
↓ 頻繁に変わる・ツリー外からも操作が必要
⑤ store(Zustand など)
① props — 一方向のデータ受け渡し
// 親から子への基本的なデータ渡し
function Parent() {
const [count, setCount] = useState(0);
return <Counter count={count} onIncrement={() => setCount((c) => c + 1)} />;
}
function Counter({ count, onIncrement }: { count: number; onIncrement: () => void }) {
return <button onClick={onIncrement}>{count}</button>;
}
props drilling は常に悪いのか?
3 階層程度の drilling は設計上の問題ではありません。読めば依存関係が明示的に追えるため、過度に嫌う必要はありません。5 階層を超えて drilling が深くなってきたら context か store を検討するサインです。
Parent → LayoutA → LayoutB → Widget → Button
← props を 4 段階渡している。このくらいが context 検討の目安
② state lifting — 兄弟コンポーネントで状態を共有
// ❌ 兄弟コンポーネントが独立して state を持っていて同期できない
function TabA() {
const [selected, setSelected] = useState<string | null>(null);
...
}
function TabB() {
const [selected, setSelected] = useState<string | null>(null); // TabA と別の state
...
}
// ✅ 共通の親に state を上げる(state lifting)
function Parent() {
const [selected, setSelected] = useState<string | null>(null);
return (
<>
<TabA selected={selected} onSelect={setSelected} />
<TabB selected={selected} />
</>
);
}
lifting のサインと限界:
- 兄弟間で同じ状態を読み書きしたくなったら lifting のタイミング
- 親が複数の子の state を持つようになり肥大化してきたら、custom hook への切り出しか store の検討サイン
③ custom hook — ロジックの再利用
// ロジックを custom hook に切り出す
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const increment = () => setCount((c) => c + 1);
const reset = () => setCount(initial);
return { count, increment, reset };
}
// 複数のコンポーネントで使える
function CounterA() {
const { count, increment } = useCounter(0); // CounterA 専用の state
return <button onClick={increment}>{count}</button>;
}
function CounterB() {
const { count, increment } = useCounter(10); // CounterB 専用の state(CounterA とは別)
return <button onClick={increment}>{count}</button>;
}
custom hook は状態を「共有」しない
これは重要な誤解です。同じ custom hook を 2 つのコンポーネントで呼んでも、それぞれ独立した state が作られます。状態の「再利用」ではなく「ロジックの再利用」です。
// CounterA と CounterB は同じ useCounter を使っているが、
// count は別々の値を持つ。一方の increment が他方に影響しない
CounterA: count = 3
CounterB: count = 12 ← 別の state
custom hook に切り出すサイン:
- 同じ state + 副作用のパターンが複数コンポーネントに重複している
- useState + useEffect + ハンドラ関数がセットになっている
- コンポーネントのレンダリングロジックとデータ取得ロジックを分けたい
④ context — ツリーへの広域配布
// テーマ・ロケールなど変更頻度が低い値に向く
const ThemeContext = createContext<"light" | "dark">("light");
function App() {
const [theme, setTheme] = useState<"light" | "dark">("light");
return (
<ThemeContext.Provider value={theme}>
<Layout />
</ThemeContext.Provider>
);
}
// ツリーのどこでも取得できる
function Button() {
const theme = useContext(ThemeContext);
return <button className={theme === "dark" ? "bg-gray-800" : "bg-white"}>...</button>;
}
context を載せすぎない考え方
context は Provider の子ツリー全体に値を提供しますが、値が変わると Provider の子が全て再レンダリングされます。
// ❌ 頻繁に変わる値を context に入れると再レンダリングが広がる
const AppContext = createContext({ user: null, count: 0, items: [] });
function AppProvider({ children }) {
const [count, setCount] = useState(0); // count が変わるたびに全子が再レンダリング
const [items, setItems] = useState([]);
return (
<AppContext.Provider value={{ count, items }}>
{children}
</AppContext.Provider>
);
}
// ✅ 変更頻度で context を分ける
const UserContext = createContext(null); // 変わらない(ログイン情報)
const ThemeContext = createContext("light"); // 変わらない
// count / items など頻繁に変わるものは Zustand などに移す
context が向く場面 / 向かない場面:
| 向く | 向かない |
|---|---|
| テーマ(light / dark) | カート数量・いいね数など頻繁に変わる数値 |
| ロケール(ja / en) | フォームの入力値 |
| ログインユーザー情報(セッション中変わらない) | リアルタイムで変わるリスト |
判断基準まとめ
Q1. データを渡す相手は直接の子コンポーネントか?
Yes → props を使う
Q2. 兄弟コンポーネント間で同じ状態を共有したいか?
Yes → state を共通の親に lifting する
Q3. 同じ state + ロジックのパターンが複数コンポーネントに重複しているか?
Yes → custom hook に切り出す(ただし状態は共有されない)
Q4. ツリーの深い階層に渡したい・変更頻度が低い(テーマ・ロケール)か?
Yes → context を使う
Q5. 頻繁に変わる・ツリー外からも操作が必要・context だと再レンダリングが多すぎるか?
Yes → Zustand などの store を使う
よくある誤用
custom hook で状態を共有しようとする
// ❌ 「custom hook を使えば状態が共有できる」という誤解
function useSharedCount() {
const [count, setCount] = useState(0); // 呼んだコンポーネントごとに独立する
return { count, increment: () => setCount((c) => c + 1) };
}
// ComponentA と ComponentB が別々の count を持つ(共有されない)
function ComponentA() { const { count } = useSharedCount(); ... }
function ComponentB() { const { count } = useSharedCount(); ... } // 別の count
// ✅ 共有が必要なら context か store を使う
context に何でも入れる
// ❌ 全 state を 1 つの context にまとめる
const GlobalContext = createContext({
user: null,
theme: "light",
cartItems: [],
searchQuery: "",
isModalOpen: false,
});
// → 何か 1 つ変わるだけで全コンポーネントが再レンダリングされる
// ✅ 変更頻度と関心ごとで context を分離する
const UserContext = createContext(null);
const ThemeContext = createContext("light");
// cartItems / searchQuery / isModalOpen → Zustand に移す
props drilling を早まって context に移す
// 3 階層の drilling は問題ない
// <Page> → <Section> → <Widget count={count} />
// context に移すと、次に同じコンポーネントを別の場所で使ったときに
// Provider が必要になり、逆に扱いにくくなることがある
// 判断基準: 5 階層以上 かつ 渡しているデータが変わりにくいなら context を検討
頻繁に変わるグローバル状態の設計 → Zustand / store 設計パターン
context + state の実装例 → react-context-global-state
テーマ切り替えの実装例 → react-context-theme