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