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

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

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

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

難易度: 中級·更新: 2026-04-17·
zustand

対応バージョン

nextjs 15react 19zustand 5

前提環境

Zustand の基本的な store 定義と useStore の使い方を理解していること

概要

Zustand の store をセレクタなしで参照すると、store 内の任意のフィールドが変わるたびにコンポーネントが再レンダリングされる。 セレクタ関数で参照するフィールドを絞ることで、そのフィールドが変わった場合のみ再レンダリングされる。

インストール

npm install zustand

実装例

// src/store/cartStore.ts
import { create } from "zustand";

type CartItem = { id: number; name: string; price: number };

type CartStore = {
  items: CartItem[];
  isOpen: boolean;
  addItem: (item: CartItem) => void;
  toggleCart: () => void;
};

export const useCartStore = create<CartStore>((set) => ({
  items: [],
  isOpen: false,
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
  toggleCart: () => set((state) => ({ isOpen: !state.isOpen })),
}));
// src/components/CartBadge.tsx — items.length だけ参照(items が変わったときのみ再レンダリング)
"use client";

import { useCartStore } from "@/store/cartStore";

export function CartBadge() {
  const itemCount = useCartStore((state) => state.items.length);

  return (
    <div className="relative">
      <span className="text-sm">カート</span>
      {itemCount > 0 && (
        <span className="absolute -right-2 -top-2 flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-xs text-white">
          {itemCount}
        </span>
      )}
    </div>
  );
}
// src/components/CartDrawer.tsx — 複数フィールドを取得する場合は useShallow
"use client";

import { useCartStore } from "@/store/cartStore";
import { useShallow } from "zustand/react/shallow";

export function CartDrawer() {
  // オブジェクトとして複数フィールドを取得する場合は useShallow で shallow 比較
  const { items, isOpen } = useCartStore(
    useShallow((state) => ({ items: state.items, isOpen: state.isOpen }))
  );

  if (!isOpen) return null;

  return (
    <div className="fixed right-0 top-0 h-full w-72 border-l bg-white p-4 shadow-lg">
      <h2 className="mb-4 font-bold">カート</h2>
      {items.length === 0 ? (
        <p className="text-sm text-gray-400">アイテムがありません</p>
      ) : (
        <ul className="space-y-2">
          {items.map((item) => (
            <li key={item.id} className="flex justify-between text-sm">
              <span>{item.name}</span>
              <span>¥{item.price}</span>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

ポイント

  • useCartStore((state) => state.items.length) のように primitive を返すセレクタは参照等価で比較される
  • 複数フィールドをオブジェクトとして返すセレクタは useShallow でラップして shallow 比較にする
  • アクション(toggleCart 等)は store 生成時から参照が変わらないため、セレクタなしで取得しても安全
  • セレクタを使わず useCartStore() 全体を参照すると、isOpen だけ変わっても全コンポーネントが再レンダリングされる

注意点

セレクタなしで useStore() 全体を参照すると、store の任意のフィールドが変わるたびに再レンダリングが発生する。セレクタで参照するフィールドを絞ると、そのフィールドが変わったときだけ再レンダリングされる。オブジェクトを返すセレクタは useShallow で shallow 比較が必要。

関連サンプル

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

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

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

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

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

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

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

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

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

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

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

関連仕様

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

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