© TOOLS BOX — Next.js / React / TypeScript コードサンプル集

サンプルガイド
←サンプル一覧
nextjsstate-management

Zustand Test のアクションを selector → props で安全に受け渡す

Zustand のアクションは store 生成時から参照が安定している性質を活かし、selector で取り出して props で子に渡す書き方を解説する。

難易度: 初級·更新: 2026-05-07·
zustand

対応バージョン

nextjs 15react 19zustand 5

前提環境

Zustand の selector と React の props の基本を理解していること

概要

Zustand のアクションは create の中で 1 度だけ作られ、store の寿命が続く限り参照が変わりません。つまり「useStore((s) => s.someAction) で取り出した関数は、props で渡しても再レンダリングを誘発しない」が成り立ちます。

Zustand Test ではこの性質を踏まえ、アクションは selector で取り出して props で子に渡す 設計を採っています。子は「props として渡された関数を呼ぶだけ」になり、store の存在を知らずに済みます。

インストール

npm install zustand

親で取り出して子に渡す基本形

「保存ボタン」だけを子コンポーネントに分けても、親が再レンダリングする回数は増えません。アクションの参照は不変だからです。

// src/components/SettingsForm.tsx
"use client";
import { useSettingsStore } from "@/store/settingsStore";
import { SaveButton } from "./SaveButton";

export function SettingsForm() {
  const setFormData = useSettingsStore((s) => s.setFormData);

  return (
    <form>
      {/* ...入力欄... */}
      <SaveButton onSave={setFormData} />
    </form>
  );
}
// src/components/SaveButton.tsx
"use client";
import type { FormData } from "@/schemas/formSchema";

type Props = {
  onSave: (values: FormData) => void;
};

export function SaveButton({ onSave }: Props) {
  return <button type="submit">保存</button>;
}

子コンポーネントは Zustand を import しません。「onSave という関数を持つだけ」 に閉じるため、テストで onSave={vi.fn()} を渡すのが容易になります。

なぜ参照が不変か

Zustand の create は次のような形で state と action を 1 度だけ作ります。

export const useSettingsStore = create<SettingsState>((set) => ({
  /* state */
  patchFormData: (partial) => set(partial),
  setFormData:   (values)  => set(values),
  reset:         ()        => set(INITIAL_SETTINGS),
}));

patchFormData などの関数オブジェクトは このコールバックが 1 度実行された時 にだけ生成されます。それ以降は同じ参照が s.patchFormData から返り続けるため、selector の戻り値は変わりません。

state と action は別 selector に分ける

state とアクションを 1 つのオブジェクトで返す書き方は、useShallow を忘れると毎レンダリングで再描画が走ります。アクションは 個別の selector で取り出す方が事故が少ないです。

// ❌ shallow なしでオブジェクトを返すと毎回別参照
const { name, setFormData } = useSettingsStore((s) => ({
  name:        s.name,
  setFormData: s.setFormData,
}));

// ✅ 個別の selector
const name        = useSettingsStore((s) => s.name);
const setFormData = useSettingsStore((s) => s.setFormData);

selector を分けると、name の変化はその行を含むコンポーネントだけに局所化されます。

useEffect の依存配列にアクションを入れて良い

「アクションが依存配列に入っていると毎回走るのでは?」と心配になりますが、参照が不変なので 依存配列に入れて構いません。むしろ ESLint の react-hooks/exhaustive-deps ルールが満たされるため、書いておくのが推奨です。

"use client";
import { useEffect } from "react";
import { useSettingsStore } from "@/store/settingsStore";
import { useFormContext } from "react-hook-form";
import type { FormData } from "@/schemas/formSchema";

export function FormWatcher() {
  const patchFormData = useSettingsStore((s) => s.patchFormData);
  const { watch } = useFormContext<FormData>();

  useEffect(() => {
    const sub = watch((value) => {
      patchFormData({
        name:  value.name  ?? "",
        email: value.email ?? "",
        rate:  Math.min(100, Math.max(0, Number(value.rate) || 0)),
      });
    });
    return () => sub.unsubscribe();
  }, [watch, patchFormData]); // ← どちらも安定している
  return null;
}

watch も patchFormData も参照が安定しているため、この useEffect は マウント時に 1 回だけ走り、unmount 時に cleanup されます。

子で store を直接参照したくなったら

「子から store を直接読みに行きたくなった」ら、その時点で 設計の見直しサイン です。基準は次のとおり。

状況推奨
子は値を表示したいだけ親で selector して props で渡す
子だけが見る重い派生 / 複数の selector子で useStore を直接呼ぶ
子は値を書き戻したいだけアクションを props で渡す
子が複数 store を組み合わせる子で個別に useStore を呼ぶ

「props で 5 個以上の値を渡している」「同じ値を多段でバケツリレーしている」 場合は、子で useStore を直接呼ぶ方が読みやすくなります。

React の外で呼ぶときは getState()

イベントハンドラの外(例: setTimeout の中・購読のリスナー内)でアクションを呼びたいケースは、useStore.getState().action(...) を使います。selector ではないので再レンダリングを起こしません。

useSettingsStore.subscribe(
  (s) => s.rate,
  (rate) => {
    useDetailStore.getState().setSuggestedRate(Math.max(0, rate * 1.1));
  }
);

これは storeSync で多用するパターンです。詳しくは 派生状態編 を参照してください。

ポイント

  • Zustand のアクションは store 生成時から 参照が不変。props で渡しても再レンダリングを誘発しない
  • state とアクションは 個別の selector で取り出す。同居させるなら useShallow を必ず付ける
  • アクションを useEffect の依存配列に入れて良い。ESLint の警告も消え、意図が明確になる
  • 子で複数の値や派生が必要になったら、props バケツリレーをやめて子で useStore を直接呼ぶ
  • React の外で呼ぶときは useStore.getState().action(...)。storeSync のリスナーや非同期コールバックで使う

関連記事

セットで読むと文脈がつながる関連記事は次のとおりです。

  • Zustand 編 — アクションが定義される 4 store の全体像
  • RHF 編 — patchFormData を useEffect 依存に入れる実例
  • uiStore 編 — setTab を子 TabBar に渡すパターン
  • Zustand Test (Build) — 完成例と外部デモ

注意点

Playground 追加記事。アクション参照の安定性を前提にした受け渡しと、それでも store を直接参照したい場合の判断基準を整理する。

リポジトリを見る

関連サンプル

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

  • Zustand Test の Store Monitor サイドパネルを selector だけで作る

    全 store の値をリアルタイムに監視するサイドパネルを selector だけで構成し、再レンダリング範囲を局所化するパターンを解説する。

  • Zustand Test の Form / Monitor / TabBar を Storybook で分離する

    Zustand に依存する UI を Storybook で取り扱うために、store のリセット / decorator / play function を組み合わせて状態をストーリー単位で固定する手順を解説する。

  • Zustand Test の uiStore でタブ切り替えだけを切り出す

    アクティブなタブの状態だけを抱える uiStore を独立させ、settings / profile / detail と疎結合に保つ最小設計を解説する。

  • Zustand Test の useShallow と primitive selector を使い分ける

    オブジェクト selector で useShallow を使う場合と、複数の primitive selector に分ける場合の判断基準を、再レンダリング挙動と一緒に解説する。

  • Next.js 15 + Zustand でモーダル状態管理を作る

    Zustand を使ってモーダルの開閉状態をグローバルに管理する実装例。複数モーダルへの対応と型安全な設計を示す。

関連仕様

このサンプルを理解するのに役立つ仕様や概念

  • FrameworkNext.jsReact ベースのフルスタックフレームワーク。SSR・SSG・App Router・API Routes を提供する。
←サンプル一覧に戻る