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

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

Zustand + Immer で immutable な状態更新をシンプルに書く

Zustand の immer middleware を使い、ネストした状態をミュータブルな書き方で安全に更新する実装例。

難易度: 中級·更新: 2026-04-16·
zustandimmer

対応バージョン

nextjs 15react 19zustand 5immer 10

前提環境

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

概要

Zustand の immer middleware を使うと、ネストした状態をスプレッド演算子なしでミュータブルに書きながら、実際には immutable な更新が行われる。 ネストが深いオブジェクトを扱うときのコードを大幅に簡潔にできる。

インストール

npm install zustand immer

ストア定義

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

type Item = {
  id: string;
  name: string;
  quantity: number;
};

type CartState = {
  items: Item[];
  addItem: (item: Item) => void;
  incrementQuantity: (id: string) => void;
  removeItem: (id: string) => void;
};

export const useCartStore = create<CartState>()(
  immer((set) => ({
    items: [],

    addItem: (item) =>
      set((state) => {
        state.items.push(item);
      }),

    incrementQuantity: (id) =>
      set((state) => {
        const item = state.items.find((i) => i.id === id);
        if (item) item.quantity += 1;
      }),

    removeItem: (id) =>
      set((state) => {
        state.items = state.items.filter((i) => i.id !== id);
      }),
  }))
);

コンポーネントでの使用

// src/components/Cart.tsx
"use client";

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

export function Cart() {
  const { items, incrementQuantity, removeItem } = useCartStore();

  if (items.length === 0) return <p className="text-sm text-gray-500">カートは空です</p>;

  return (
    <ul className="space-y-2">
      {items.map((item) => (
        <li key={item.id} className="flex items-center gap-4 rounded border px-4 py-2 text-sm">
          <span className="flex-1">{item.name}</span>
          <span>×{item.quantity}</span>
          <button
            onClick={() => incrementQuantity(item.id)}
            className="rounded bg-gray-100 px-2 py-1 hover:bg-gray-200"
          >
            +
          </button>
          <button
            onClick={() => removeItem(item.id)}
            className="rounded bg-red-100 px-2 py-1 text-red-600 hover:bg-red-200"
          >
            削除
          </button>
        </li>
      ))}
    </ul>
  );
}

ポイント

  • immer middleware は zustand/middleware/immer からインポートする(zustand/middleware 直下の immer は Zustand v4 以前の書き方)
  • set((state) => { state.items.push(...) }) のようにミュータブルに書けるが、実際には Immer が immutable な更新に変換する
  • スプレッド演算子でネストを展開する必要がなくなり、深いオブジェクトの更新が読みやすくなる
  • create<CartState>()(immer(...)) の二重括弧は TypeScript で型推論を正しく効かせるための Zustand v5 の記法

注意点

Zustand v5 では immer middleware のインポートパスが変わっているため注意。`zustand/middleware` からインポートする。

関連サンプル

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

  • Zustand の persist middleware でローカルストレージに状態を保存する

    persist middleware を使い、Zustand の状態をローカルストレージに永続化してページリロード後も状態を維持する実装例。

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

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

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

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

  • Context と CSS カスタムプロパティでテーマシステムを構築する

    React Context で現在のテーマを管理し、CSS カスタムプロパティ(CSS 変数)でコンポーネントがテーマを自動追従するシステムを実装する例。

  • React でよく使うフィルタ条件をプリセットとして保存・再適用する

    検索条件(キーワード・カテゴリ・難易度など)をプリセットとして localStorage に保存し、一覧画面からワンクリックで再適用できる UI パターン。

関連仕様

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

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