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

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

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

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

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

対応バージョン

nextjs 15react 19zustand 5

前提環境

Zustand の selector と useShallow の使い方を理解していること

概要

Zustand Test の右サイドには、4 つの store の値を 常時表示する Store Monitor があります。 これは Redux DevTools とは別に、画面の中にいつでも見える形で置いておくと、フォーム入力 → store 投影 → 同期、という流れを体感しやすいからです。

設計方針は3つです。

  • Monitor 専用の selector を store ごとに用意し、表示領域だけを購読する
  • オブジェクトを返す selector は useShallow で包んで再レンダリングを抑える
  • Monitor の更新と本体 UI の更新を分離 する。Monitor 由来の更新が本体に波及しないようにする

インストール

npm install zustand

1 つの store を表示する MonitorSection

最小単位は「1 つの store の中身を <pre> で出すコンポーネント」です。useShallow でラップしておけば、関係のないフィールドの変化では再レンダリングされません。

// src/components/monitor/SettingsMonitor.tsx
"use client";
import { useShallow } from "zustand/react/shallow";
import { useSettingsStore } from "@/store/settingsStore";

export function SettingsMonitor() {
  const snapshot = useSettingsStore(
    useShallow((s) => ({
      name:  s.name,
      email: s.email,
      rate:  s.rate,
    }))
  );

  return (
    <section>
      <h3>settingsStore</h3>
      <pre>{JSON.stringify(snapshot, null, 2)}</pre>
    </section>
  );
}

action はあえて含めません。Monitor は 値を見たいだけ であり、関数の参照差分まで監視する必要がないためです。

4 つの store を並べる Monitor 本体

各 store について同じ形の Monitor を作り、サイドパネルとして並べます。

// src/components/monitor/StoreMonitor.tsx
"use client";
import { SettingsMonitor } from "./SettingsMonitor";
import { ProfileMonitor }  from "./ProfileMonitor";
import { DetailMonitor }   from "./DetailMonitor";
import { UiMonitor }       from "./UiMonitor";

export function StoreMonitor() {
  return (
    <aside className="space-y-3 p-3 text-xs">
      <SettingsMonitor />
      <ProfileMonitor />
      <DetailMonitor />
      <UiMonitor />
    </aside>
  );
}

StoreMonitor 自体は 何も購読しません。各 Monitor が自分の store だけを購読しているため、SettingsMonitor の再レンダリングは ProfileMonitor に波及しません。

レイアウトに置く

タブエリアの右に Monitor を常時並べます。タブを切り替えてもサイドパネルは消えません。

// src/app/zustand-demo/page.tsx
import { TabbedArea }   from "@/components/TabbedArea";
import { StoreMonitor } from "@/components/monitor/StoreMonitor";

export default function ZustandDemoPage() {
  return (
    <div className="grid grid-cols-[1fr_280px] gap-4">
      <TabbedArea />
      <StoreMonitor />
    </div>
  );
}

タブが切り替わっても StoreMonitor は再マウントされないので、購読が張り直される無駄が起きません。

なぜ useStore() 全部購読を避けるか

useSettingsStore() を引数なしで呼ぶと state 全体を購読してしまい、1 文字入力するたびに Monitor が再レンダリング されます。watch 経由でリアルタイム同期している Playground ではこれが特に致命的です。

selector で値を絞ると、変化したフィールドのみが Monitor の再描画を引き起こします。rate だけ変わったときに name の <pre> まで再描画されることを避けられます。

// ❌ 避ける
const all = useSettingsStore();

// ✅ 推奨
const snapshot = useSettingsStore(
  useShallow((s) => ({ name: s.name, email: s.email, rate: s.rate }))
);

getState() ではなく selector を使う理由

「ただ値を読むだけなら useSettingsStore.getState().name でもいいのでは?」という疑問が出ますが、それでは store の変化を Monitor が拾えません。getState() は呼んだ瞬間の値を一回返すだけで、Zustand から再レンダリング通知が来ません。 Monitor は 変化のたびに再描画したい コンポーネントなので、必ず useStore(selector) で購読します。

派生状態を Monitor で見るときは個別 selector

detailStore.suggestedRate のような 派生値 も同じ要領で表示できます。派生の作り方は 派生状態編 を参照してください。

// src/components/monitor/DetailMonitor.tsx
"use client";
import { useShallow } from "zustand/react/shallow";
import { useDetailStore } from "@/store/detailStore";

export function DetailMonitor() {
  const snapshot = useDetailStore(
    useShallow((s) => ({
      mode:          s.mode,
      suggestedRate: s.suggestedRate,
    }))
  );

  return (
    <section>
      <h3>detailStore</h3>
      <pre>{JSON.stringify(snapshot, null, 2)}</pre>
    </section>
  );
}

suggestedRate が変わるのは settingsStore.rate の変化に追随する瞬間です。Monitor 上でそのタイミングが目に見えるため、storeSync の挙動を理解する助けになります。

ポイント

  • Monitor は store ごとに 1 コンポーネント に分け、useShallow で必要なフィールドだけ購読する
  • 親(StoreMonitor)は何も購読せず、子の再レンダリングが互いに波及しない構造にする
  • useStore() 全部購読は避ける。1 文字入力ごとに Monitor 全体が再描画される
  • 値を「読むだけ」なら getState() でも取れるが、変化を追えないため Monitor では使わない
  • 派生 store も同じ要領で表示する。同期タイミングが目に見えるため、storeSync のデバッグが楽になる

関連記事

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

  • Zustand 編 — 監視対象の 4 store と storeSync の設計
  • 派生状態編 — Monitor に映る派生値の作り方
  • selector 最適化 — useShallow と primitive selector の使い分け
  • Zustand Test (Build) — 完成例と外部デモ

注意点

Playground 右サイドの Store Monitor を実装する記事。devtools とは別に、画面上に常時表示する『生きたデバッグパネル』のレシピ。

リポジトリを見る

関連サンプル

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

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

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

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

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

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

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

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

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

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

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

関連仕様

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

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