TOOLS BOX/ガイド/グローバル store と private state の使い分け
Concept

グローバル store と private state の使い分け

Zustand などのグローバル store に置くデータと、コンポーネントの private state に閉じるべきデータの判断基準を整理するガイド。form state・hover・server data・derived state を store に入れるべきでない理由と、global に置くべき状態の特徴を具体例で説明する。

zustandstate-managementglobal-stateprivate-statelocal-stateserver-datareactstore

どういう場面で使うか

  • ·global store: 複数のコンポーネントから同じ状態を読み書きしたいとき
  • ·global store: コンポーネントが破棄されても状態を維持したいとき
  • ·private state: そのコンポーネント内だけで完結する表示制御・入力値・一時状態
  • ·server data: API レスポンスは TanStack Query / SWR に任せ、store には入れない

注意点 / Pitfalls

  • ·hover・focus・tooltip の開閉など、1 コンポーネント内で完結する状態を store に入れると不必要に global が汚れる
  • ·form state(入力値・エラー・isDirty)を store に入れると、バリデーションや送信状態まで store で管理することになり肥大化する
  • ·API で取得した配列を store に常駐させると、再取得・キャッシュ無効化・loading / error 状態を自前実装することになる
  • ·derived state(合計・フィルタ済みリストなど)を store に持つと元データとの二重管理になる。useMemo で計算する
  • ·selector なしで store 全体を購読すると、無関係なフィールドが変わるたびに再レンダリングが発生する

補足

「global に置く必要があるか」を先に問う。useState で解決できるなら useState、兄弟間の共有なら state lifting、ツリー全体なら context、それでも足りないときに store を選ぶ。store は状態管理の便利ツールではなく、共有要件が明確にあるときのツール。

判断の起点:「複数コンポーネントから読み書きが必要か」

この状態は複数のコンポーネントから読み書きされるか?
  No → private state(useState / useReducer)で解決する
  Yes → コンポーネントが破棄されても維持が必要か?
          No → state lifting か context を検討する
          Yes → global store(Zustand など)を使う

「グローバルに置けば便利そう」という理由だけでは store に入れません。共有の必要性が明確にあるかが判断の起点です。

global store に向くデータ

// ✅ モーダルの開閉状態(複数のボタン・ページから制御される)
type ModalStore = {
  isOpen: boolean;
  modalType: "confirm" | "edit" | null;
  open: (type: "confirm" | "edit") => void;
  close: () => void;
};

// ✅ ユーザーが選択した設定(ページ跨ぎで維持が必要)
type PreferenceStore = {
  sidebarCollapsed: boolean;
  toggleSidebar: () => void;
};

// ✅ ショッピングカート(ページ間で維持・複数コンポーネントから操作)
type CartStore = {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
};

global store に向く状態の特徴:

  • 複数のコンポーネントから読み書きされる
  • コンポーネントが破棄(アンマウント)されても維持したい
  • props drilling や state lifting では届かない深さのツリーにある

private state に閉じるべきデータ

表示制御の一時状態(hover・focus・open/close)

// ✅ ドロップダウン・ツールチップの開閉 → そのコンポーネント内で完結する
function Dropdown() {
  const [isOpen, setIsOpen] = useState(false); // private state で十分

  return (
    <div>
      <button onClick={() => setIsOpen((o) => !o)}>開く</button>
      {isOpen && <ul>...</ul>}
    </div>
  );
}

// ❌ store に入れると global が汚れる上、他のコンポーネントと競合しやすい
const useDropdownStore = create(() => ({ isOpen: false }));

判断基準: そのコンポーネントが画面から消えたら状態も消えてよいか? → Yes なら private state

form state

// ✅ react-hook-form に任せる
function LoginForm() {
  const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm();
  // 入力値・エラー・isDirty・isSubmitting はすべて form ライブラリ内に閉じる
}

// ❌ store に入れると送信後のリセット・バリデーション連携が複雑になる
type FormStore = {
  email: string;
  password: string;
  errors: Record<string, string>;
  setField: (key: string, value: string) => void;
};

form state の問題点は「送信後のリセット」「バリデーション状態」「isDirty / isTouched」まで store で管理する必要が生まれることです。react-hook-form はこれらを一括して扱う専用ツールです。

hover / focus などのインタラクション状態

// ✅ hover は useRef / CSS :hover / useState で完結させる
function Card() {
  const [hovered, setHovered] = useState(false);
  return (
    <div
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      className={hovered ? "shadow-lg" : ""}
    >
      ...
    </div>
  );
}

// ❌ hover 状態を global store に置く理由はほぼない

server data を store に常駐させるべきでない場面

❌ store で API キャッシュを管理しようとすると:
  ・再取得タイミングの管理(stale になったら?)
  ・loading / error 状態の管理
  ・複数画面でのキャッシュ共有
  ・リクエスト重複の制御
  これらをすべて自前実装することになる

✅ TanStack Query / SWR に任せると:
  ・キャッシュ・再取得・loading / error を自動管理
  ・staleTime / cacheTime で鮮度を制御できる
  ・楽観更新も onMutate で実装できる
// ❌ API データを store に常駐させるパターン
const useProductStore = create((set) => ({
  products: [] as Product[],
  isLoading: false,
  fetchProducts: async () => {
    set({ isLoading: true });
    const data = await api.getProducts();
    set({ products: data, isLoading: false });
  },
}));

// ✅ TanStack Query に任せる
function ProductList() {
  const { data: products, isLoading } = useQuery({
    queryKey: ["products"],
    queryFn: api.getProducts,
    staleTime: 60_000,
  });
}

例外: サーバーから取得した後にユーザーが編集・並び替えなどの操作を行い、その結果をローカルで保持するケースは store が適することがあります。「取得したデータをそのまま表示する」なら fetch ライブラリに任せ、「取得後に加工してローカルで管理する」なら store の検討対象になります。

derived state を store に持つべきでない

// ❌ total を store に持つ → items と二重管理になる
type CartStore = {
  items: CartItem[];
  total: number;       // items から計算できる
  addItem: (item: CartItem) => void;
  // addItem のたびに total も更新する必要がある → 更新忘れのリスク
};

// ✅ コンポーネント内で useMemo を使って計算する
function CartSummary() {
  const items = useCartStore((s) => s.items);
  const total = useMemo(
    () => items.reduce((sum, item) => sum + item.price * item.quantity, 0),
    [items]
  );
  return <p>合計: {total.toLocaleString()} 円</p>;
}

derived state を store に持つと、元データを更新するたびに derived state も更新する必要があり、更新漏れがバグの原因になります。

selector なしの広域購読で起きること

// ❌ store 全体を購読 → 無関係なフィールドが変わっても再レンダリングされる
function Header() {
  const store = useAppStore(); // isOpen / theme / user / cartItems...すべてを購読

  // cartItems が変わるだけで Header が再レンダリングされる
  return <nav>{store.user?.name}</nav>;
}

// ✅ 使うフィールドだけ select する
function Header() {
  const userName = useAppStore((s) => s.user?.name);
  // user.name が変わったときだけ再レンダリング
  return <nav>{userName}</nav>;
}

判断チェックリスト

置こうとしている状態を確認する:

□ 複数のコンポーネントから読み書きされるか?
    No → useState / useReducer で private state にする

□ コンポーネントが消えても維持する必要があるか?
    No → state lifting か context を先に検討する

□ API から取得したデータをそのまま格納したいだけか?
    Yes → TanStack Query / SWR に任せる

□ 他のフィールドから計算できる値か(合計・フィルタ結果)?
    Yes → useMemo で計算する(store には入れない)

□ そのコンポーネント専用の表示状態(hover・dropdown の開閉)か?
    Yes → private state に閉じる

□ フォームの入力値・バリデーション・送信状態か?
    Yes → react-hook-form などの form ライブラリに任せる

すべての No を通過したものだけが global store の候補

よくある誤用まとめ

状態の種類誤った配置正しい配置
hover / focus / dropdown 開閉global storeuseState(private)
フォームの入力値・エラーglobal storereact-hook-form
API レスポンス(表示用)global storeTanStack Query / SWR
合計・フィルタ済みリストstore の derived stateuseMemo
モーダルの開閉(複数箇所から操作)private stateglobal store ✅
テーマ・ロケール(ツリー全体)global store(更新頻度次第)context でも可

store 設計の全体像 → Zustand / store 設計パターン

データ受け渡し手段の選択 → React Hook によるデータ受け渡しパターン

selector と再レンダリング制御の実装例 → zustand-selector-optimization

関連ドキュメント

関連サンプル

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