TOOLS BOX/ガイド/React Hook によるデータ受け渡しパターン
Concept

React Hook によるデータ受け渡しパターン

コンポーネント間のデータ受け渡しを props / state lifting / custom hook / context の 4 段階で整理するガイド。props drilling の許容範囲、custom hook が状態を共有しない理由、context を載せすぎない判断基準、Zustand 導入前に React 標準で整理できる範囲を明確にする。

reactpropsstate-liftingcustom-hookcontextuseContextdata-passingcomponent-design

どういう場面で使うか

  • ·props: 親→子の一方向でデータを渡す。1〜2 階層なら props で十分
  • ·state lifting: 兄弟コンポーネント間で状態を共有したいとき(共通の親に state を上げる)
  • ·custom hook: ロジックを再利用したいとき(状態自体は各コンポーネントで独立する)
  • ·context: ツリー全体に渡したいが変更頻度が低いもの(テーマ・ロケール・認証ユーザー情報)
  • ·store(Zustand): React ツリー外からも操作が必要、または context だと再レンダリングが多すぎるとき

注意点 / Pitfalls

  • ·custom hook は状態を「共有」しない。同じ custom hook を複数コンポーネントで呼んでも、それぞれ独立した state が作られる
  • ·context に頻繁に変わる値を入れると、Provider の子ツリー全体が再レンダリングされる
  • ·props drilling は 3 階層程度までは設計の問題ではない。早まって context に移すと読みにくくなることがある
  • ·state lifting で親が太ってきたら、複数の子 state を持つより custom hook に切り出すか store の検討サイン
  • ·context で解決しようとしている問題が「更新頻度の高い状態の広域共有」なら Zustand が適している

補足

判断の順序は props → state lifting → custom hook → context → store。前の段階で解決できるなら移行しない。store は最後の手段ではなく、要件に合えば早めに導入してよい。

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

関連ドキュメント

関連サンプル

同じテーマや技術スタックを使った実装例