レビュー済み·難易度: 中級·更新: 2026-04-17

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

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

nextjsstate-managementperformancezustand

対応バージョン

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 比較が必要。

関連サンプル