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

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

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

nextjsstate-managementzustandimmer

対応バージョン

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` からインポートする。

関連サンプル