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

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

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

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

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

対応バージョン

nextjs 15react 19zustand 5

前提環境

Zustand の selector の基本(useStore に関数を渡す)を理解していること

概要

Zustand Test では、selector の戻り値の 型(primitive か object か) に応じて、再レンダリング比較の方法を 2 通り使い分けています。

selector の戻り値比較方法書き方
primitive(string / number)===(参照等価)そのまま useStore((s) => s.name)
object / arrayshallow(1階層)useStore(useShallow((s) => ({ ... })))
複数の値が欲しい個別 selectorconst a = useStore(...); const b = ...;

選択を間違えると、入力 1 文字ごとに無関係なコンポーネントが再描画 されたり、逆に意図したタイミングで更新されなかったりします。本記事ではそれぞれの判断基準を整理します。

インストール

npm install zustand

primitive selector はそのままで OK

返す値が string / number / boolean なら、Zustand のデフォルト比較(===)で十分です。同じ値が返るなら同じ参照なので、再レンダリングは起きません。

const name        = useSettingsStore((s) => s.name);
const rate        = useSettingsStore((s) => s.rate);
const setFormData = useSettingsStore((s) => s.setFormData);

action(関数)も参照が安定しているため、同様に primitive 扱い で OK です(アクション受け渡し編 参照)。

オブジェクトを返すなら useShallow を必ず付ける

複数フィールドを 1 つのオブジェクトとして取り出すと、selector は 毎回新しいオブジェクト を返します。デフォルトの === 比較では「内容が同じでも別参照」となり、関係ないフィールドが変わるたびに再レンダリングが走ります。

import { useShallow } from "zustand/react/shallow";

// ❌ shallow なし → 毎レンダリングで再描画
const { name, email } = useSettingsStore((s) => ({ name: s.name, email: s.email }));

// ✅ useShallow でラップ
const { name, email } = useSettingsStore(
  useShallow((s) => ({ name: s.name, email: s.email }))
);

useShallow は オブジェクトの 1 階層を === で比較 します。ネストはたどらないので、ネストしたオブジェクトを返す selector では効きません。

ありがちな失敗例

ネストしたオブジェクトをそのまま返してしまうと、useShallow を付けても効果がありません。

// ❌ ネストを返す → useShallow でも内側を比較できない
const { profile } = useSettingsStore(
  useShallow((s) => ({ profile: { name: s.name, email: s.email } }))
);

profile キーの値(オブジェクト)は 毎回別参照 になるため、shallow 比較でも「変わった」と判定されます。解決策は 2 つ。

// 1. フラットにする
const { name, email } = useSettingsStore(
  useShallow((s) => ({ name: s.name, email: s.email }))
);

// 2. 個別に取る
const name  = useSettingsStore((s) => s.name);
const email = useSettingsStore((s) => s.email);

「useShallow を付けたのに毎回再描画される」ときは、1 階層よりも深いオブジェクトが selector の戻り値に混ざっていないか を最初に疑います。

個別 selector とオブジェクト selector の判断

どちらが速いかは状況依存ですが、判断基準の優先順位 はおおむね次のとおりです。

  1. 取り出すフィールドが 1〜3 個 → 個別 selector が読みやすい
  2. 4 個以上をまとめて使う → useShallow でオブジェクト selector
  3. JSX の中で破壊代入したい → useShallow で破壊代入の形 を選ぶ
// 個別の方が読みやすい例
const name        = useSettingsStore((s) => s.name);
const setFormData = useSettingsStore((s) => s.setFormData);

// オブジェクトの方が読みやすい例(破壊代入で並べたい)
const { name, email, rate, setFormData } = useSettingsStore(
  useShallow((s) => ({
    name:        s.name,
    email:       s.email,
    rate:        s.rate,
    setFormData: s.setFormData,
  }))
);

「個別 selector を 5 行並べると読みづらい」 と感じたら、オブジェクト selector + useShallow に切り替えると整理されます。

派生値を計算する selector

selector の中で計算してから返す場合、戻り値が primitive ならそのままで OK です。重い計算でなければ 毎レンダリングで再計算しても問題ありません。

// 計算しても primitive なら useShallow 不要
const suggested = useSettingsStore((s) => Math.max(0, s.rate * 1.1));

計算がオブジェクト を返す場合は、useShallow を使うか、別 store に書き出す (派生状態編) を検討します。

// ❌ 毎回新しいオブジェクト
const range = useSettingsStore((s) => ({ min: 0, max: s.rate * 1.1 }));

// ✅ useShallow で内容比較
const range = useSettingsStore(
  useShallow((s) => ({ min: 0, max: s.rate * 1.1 }))
);

Store Monitor での実例

Store Monitor 編 では、useShallow を 必ず 使っています。Monitor は複数フィールドを <pre> でまとめて表示するため、自然とオブジェクト selector になるためです。

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

patchFormData が 1 文字ごとに走る Playground では、ここを useShallow なしで書くと 入力のたびに Monitor の <pre> が再描画されまくる ことになります。

ポイント

  • selector の戻り値が primitive ならデフォルトの === 比較で OK。useShallow は不要
  • オブジェクトを返すなら useShallow を必ず付ける。1 階層しか比較しない点に注意
  • ネストしたオブジェクトを返すと useShallow でも防げない。フラットにするか個別 selector に分ける
  • 取り出すフィールドが 1〜3 個 なら個別 selector、4 個以上 なら useShallow + オブジェクトが読みやすい
  • selector の中の計算はそれ自体は軽い。戻り値の型 で useShallow 要否を判断する

関連記事

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

  • Zustand 編 — selector で再描画範囲を絞る基本
  • Store Monitor 編 — useShallow を多用する実例
  • アクション受け渡し編 — アクション参照の安定性と個別 selector
  • Zustand Test (Build) — 完成例と外部デモ

注意点

Playground 追加記事。useShallow を「とりあえず付ける」のではなく、何を比較しているかを理解した上で使い分けるための整理。

リポジトリを見る

関連サンプル

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

  • Zustand の selector で不要な再レンダリングを防ぐ

    useStore にセレクタ関数を渡し、参照する state を絞ることで不要な再レンダリングを防ぐ実装例。

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

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

  • Zustand Test の watch 同期コストを計測して throttle で抑える

    RHF の watch から settingsStore に毎キーストローク同期する負荷を performance.mark で計測し、必要なら throttle / 差分検知で抑える手順を解説する。

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

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

  • Zustand Test の保存処理を非同期 action と loading / error 状態で扱う

    saveSettings のような非同期処理を store のアクションに閉じ込め、status: 'idle'|'loading'|'error'|'success' を 1 つのフィールドで表現する書き方を解説する。

関連仕様

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

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