8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「Zustandがあれば Context はもう要らない」──そんな声をよく聞きます。本当にそうでしょうか?

8
Posted at

Props Drilling・Context・Zustand ── 「なんとなく使ってる」を卒業するための選び方

「Zustandがあれば Context はもう要らない」──コードレビューでも X でもよく見かける意見です。結論から言うと、半分正解で半分ミスリードだと思っています。

この記事では Props Drilling → Context → Zustand の順に、仕組み・使いどころ・落とし穴を整理し、「どれを選ぶか」の判断軸を示します。


目次

  1. 前提:なぜ状態管理が必要か
  2. Props Drilling ── まずは基本を理解する
  3. React Context ── 標準の解決策
  4. Zustand ── 軽量グローバルストア
  5. 3つを横断比較
  6. どれを選ぶべきか?判断フローチャート
  7. まとめ

1. 前提:なぜ状態管理が必要か

React のデータフローは 単方向(親 → 子) です。
親が持つ状態を孫コンポーネントに届けるには、何らかの手段が必要です。

App
 └── Layout
      └── Sidebar
           └── UserAvatar  ← ここで user が欲しい

この距離が問題の本質です。


2. Props Drilling ── まずは基本を理解する

仕組み

props を「バケツリレー」のように中間コンポーネント経由で渡す方法です。

// App.tsx
function App() {
  const [user, setUser] = useState<User>({ name: "Taro", role: "admin" });
  return <Layout user={user} />;
}

// Layout.tsx
function Layout({ user }: { user: User }) {
  return <Sidebar user={user} />;
}

// Sidebar.tsx
function Sidebar({ user }: { user: User }) {
  return <UserAvatar user={user} />;
}

// UserAvatar.tsx
function UserAvatar({ user }: { user: User }) {
  return <img src={`/avatars/${user.name}.png`} alt={user.name} />;
}

LayoutSidebaruser使っていないのに受け取っています。

Props Drilling が向いている場面

  • コンポーネントの階層が 2〜3層以内
  • 状態の所有者と消費者が近い
  • ライブラリ依存をゼロにしたい小規模コンポーネント

問題点

問題 説明
中間コンポーネントの汚染 使わない props を受け取り・渡す必要がある
リファクタリングコスト props の名前や型を変えると全階層を修正
可読性の低下 コンポーネントの「本来の責務」がぼやける

Props Drilling は悪手ではありません。「辛くなってきたら移行する」 ──これが健全なアプローチです。


3. React Context ── 標準の解決策

仕組み

Context は Provider(提供)→ Consumer(消費) のパターンで、中間コンポーネントをスキップして値を届けます。

// UserContext.tsx
import { createContext, useContext, useState, ReactNode } from "react";

type User = { name: string; role: string };
type UserContextType = { user: User; setUser: (u: User) => void };

const UserContext = createContext<UserContextType | null>(null);

export function UserProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User>({ name: "Taro", role: "admin" });
  return (
    <UserContext.Provider value={{ user, setUser }}>
      {children}
    </UserContext.Provider>
  );
}

export function useUser() {
  const ctx = useContext(UserContext);
  if (!ctx) throw new Error("useUser must be used within UserProvider");
  return ctx;
}
// UserAvatar.tsx ── 中間コンポーネントへの props は不要
function UserAvatar() {
  const { user } = useUser();
  return <img src={`/avatars/${user.name}.png`} alt={user.name} />;
}

Context が向いている場面

  • テーマ(ダーク/ライト)言語(i18n)認証情報 など変化が少ない値
  • コンポーネントライブラリ内部の設定値の受け渡し
  • 追加ライブラリを使いたくないプロジェクト

Context の落とし穴:再レンダリング問題

Context の値が変わると、その Provider 配下の全 Consumer が再レンダリングされます。

// 頻繁に変わる値を一つの Context に詰め込む(よくある失敗)
const AppContext = createContext({
  user,        // 滅多に変わらない
  cartItems,   // 毎回変わる ← これが変わるたびに user を使うコンポーネントも再描画
  theme,
});

解決策:Context を分割する

Provider を分けることで、cartItems が変化しても UserProvider 配下のコンポーネントは再レンダリングされなくなります。

// 更新頻度ごとに Context を分ける
<UserProvider>      {/* 低頻度 */}
  <ThemeProvider>   {/* 低頻度 */}
    <CartProvider>  {/* 高頻度 */}
      <App />
    </CartProvider>
  </ThemeProvider>
</UserProvider>

Context は「グローバル変数の React 版」と思うと理解しやすいです。ただし、頻繁に変わるデータ(カートの中身、フォーム入力値など)には向いていません。

useMemo / useCallback でコンテキスト値を安定させるか、use-context-selector を使ってセレクタベースの購読に切り替えると再レンダリングを最小化できます。


4. Zustand ── 軽量グローバルストア

仕組み

Zustand は React の外側にストアを持ち、コンポーネントはセレクタで必要な値だけ購読します。

npm install zustand
// store/userStore.ts
import { create } from "zustand";

type User = { name: string; role: string };

interface UserStore {
  user: User;
  setUser: (user: User) => void;
}

export const useUserStore = create<UserStore>((set) => ({
  user: { name: "Taro", role: "admin" },
  setUser: (user) => set({ user }),
}));
// UserAvatar.tsx
function UserAvatar() {
  // name だけ購読 → role が変わっても再レンダリングしない
  const name = useUserStore((state) => state.user.name);
  return <img src={`/avatars/${name}.png`} alt={name} />;
}

Zustand が向いている場面

  • 頻繁に更新される状態(カート、フィルター、通知)
  • 複数の場所から読み書きされる 状態
  • Redux は重すぎるが Context の再レンダリングが気になる場面
  • React コンポーネント外(イベントハンドラ、WebSocket コールバック)から状態を更新したい

Zustand の強み

useUserStore.getState() を使えば、React の外からでもストアにアクセスできます。これは Context では実現できない Zustand ならではの強みです。

import { useUserStore } from "./store/userStore";

// WebSocket などの非 React コードから直接呼べる
function onAuthSuccess(user: User) {
  useUserStore.getState().setUser(user);
}

devtools と persist ミドルウェアの組み合わせも簡単に書けます。

import { devtools, persist } from "zustand/middleware";

const useUserStore = create<UserStore>()(
  devtools(
    persist(
      (set) => ({
        user: { name: "", role: "" },
        setUser: (user) => set({ user }),
      }),
      { name: "user-storage" } // localStorage に永続化
    )
  )
);

Zustand の注意点

  • 小規模なコンポーネントローカルな状態には過剰(普通に useState で十分)
  • ストアが増えすぎると管理が散漫になる → ドメインごとにファイルを分ける習慣が重要

5. 3つを横断比較

観点 Props Drilling Context Zustand
導入コスト ゼロ 低(React 標準) 低(軽量ライブラリ)
ボイラープレート 少〜中
再レンダリング制御 手動で制御しやすい 全 Consumer が再描画 セレクタで最小化
React 外からの更新 不可 不可
DevTools なし React DevTools のみ Redux DevTools 互換
永続化(localStorage) なし 手動実装 ミドルウェアで簡単
テストしやすさ しやすい しやすい しやすい(ストアを直接操作可)
学習コスト 最低 低〜中
向いているデータ ローカル・浅い階層 テーマ・認証・低頻度 高頻度・広範囲

6. どれを選ぶべきか?判断フローチャート

状態が必要になった
        │
        ▼
  この状態は「このコンポーネントだけ」で使う?
        │
      Yes → ロジックが複雑?
              │
            Yes → useReducer
              │
             No → useState
        │
        No
        │
        ▼
  渡す階層は 2〜3 層以内?
        │
      Yes → Props Drilling でも十分
        │
        No
        │
        ▼
  頻繁に更新される?(カート・通知・フォーム)
  または React 外から更新が必要?
        │
      Yes → Zustand
        │
        No(テーマ・言語・認証など低頻度)
        │
        ▼
      Context

実際のプロジェクトでの使い分け例

// 認証情報 → Context(アプリ全体で使うが変化は少ない)
<AuthProvider>
  {/* テーマ → Context */}
  <ThemeProvider>
    {/* カート・商品フィルター → Zustand(頻繁に変わる) */}
    <App />
  </ThemeProvider>
</AuthProvider>

Context と Zustand は競合ではなく補完の関係です。同じプロジェクトで両方使うのは普通のことです。


7. まとめ

ツール 一言で言うと
Props Drilling 「近いなら props で十分」シンプルを大切に
Context 「広く使う・変化が少ない」値の配送係
Zustand 「頻繁に変わる・広範囲」な状態のための軽量ストア

React Context は不要か?

答えは No です。
テーマ・認証・ローカライズのように「広く使われるが変化が少ない値」には Context が最適です。一方で「頻繁に変わる状態」や「React 外からの更新」が必要な場面では Zustand が輝きます。

道具を正しく使い分けることが、スケールするフロントエンドへの近道です。


参考リンク

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?