//
Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Usa el middleware immer para escribir actualizaciones de state con estilo mutable que producen state inmutable por debajo. Esto elimina el spread manual de objetos para actualizaciones anidadas.
npm install immerimport { create } from "zustand";
import { immer } from "zustand/middleware/immer";
interface Todo {
id: string;
text: string;
done: boolean;
subtasks: { id: string; text: string; done: boolean }[];
}
interface TodoStore {
todos: Todo[];
addTodo: (text: string) => void;
toggleTodo: (id: string) => void;
addSubtask: (todoId: string, text: string) => void;
toggleSubtask: (todoId: string, subtaskId: string) => void;
}
export const useTodoStore = create<TodoStore>()(
immer((set) => ({
todos: [],
addTodo: (text) =>
set((state) => {
state.todos.push({
id: crypto.randomUUID(),
text,
done: false,
subtasks: [],
});
}),
toggleTodo: (id) =>
set((state) => {
const todo = state.todos.find((t) => t.id === id);
if (todo) todo.done = !todo.done;
}),
addSubtask: (todoId, text) =>
set((state) => {
const todo = state.todos.find((t) => t.id === todoId);
if (todo) {
todo.subtasks.push({ id: crypto.randomUUID(), text, done: false });
}
}),
toggleSubtask: (todoId, subtaskId) =>
set((state) => {
const todo = state.todos.find((t) => t.id === todoId);
const subtask = todo?.subtasks.find((s) => s.id === subtaskId);
if (subtask) subtask.done = !subtask.done;
}),
}))
);// stores/spreadsheet-store.ts
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
import { devtools } from "zustand/middleware";
interface Cell {
value: string;
formula: string | null;
format: {
bold: boolean;
italic: boolean;
color: string;
backgroundColor: string;
};
}
interface SpreadsheetStore {
cells: Record<string, Cell>;
selectedCell: string | null;
setCellValue: (cellId: string, value: string) => void;
setCellFormula: (cellId: string, formula: string) => void;
formatCell: (cellId: string, format: Partial<Cell["format"]>) => void;
selectCell: (cellId: string | null) => void;
batchUpdate: (updates: { cellId: string; value: string }[]) => void;
clearCell: (cellId: string) => void;
}
const defaultCell: Cell = {
value: "",
formula: null,
format: { bold: false, italic: false, color: "#000000", backgroundColor: "#ffffff" },
};
export const useSpreadsheetStore = create<SpreadsheetStore>()(
devtools(
immer((set) => ({
cells: {},
selectedCell: null,
setCellValue: (cellId, value) =>
set((state) => {
if (!state.cells[cellId]) {
state.cells[cellId] = { ...defaultCell };
}
state.cells[cellId].value = value;
state.cells[cellId].formula = null;
}),
setCellFormula: (cellId, formula) =>
set((state) => {
if (!state.cells[cellId]) {
state.cells[cellId] = { ...defaultCell };
}
state.cells[cellId].formula = formula;
}),
formatCell: (cellId, format) =>
set((state) => {
if (!state.cells[cellId]) {
state.cells[cellId] = { ...defaultCell };
}
Object.assign(state.cells[cellId].format, format);
}),
selectCell: (cellId) =>
set((state) => {
state.selectedCell = cellId;
}),
batchUpdate: (updates) =>
set((state) => {
for (const { cellId, value } of updates) {
if (!state.cells[cellId]) {
state.cells[cellId] = { ...defaultCell };
}
state.cells[cellId].value = value;
}
}),
clearCell: (cellId) =>
set((state) => {
delete state.cells[cellId];
}),
})),
{ name: "SpreadsheetStore" }
)
);// components/cell.tsx
"use client";
import { useSpreadsheetStore } from "@/stores/spreadsheet-store";
export function Cell({ cellId }: { cellId: string }) {
const cell = useSpreadsheetStore((s) => s.cells[cellId]);
const selectedCell = useSpreadsheetStore((s) => s.selectedCell);
const setCellValue = useSpreadsheetStore((s) => s.setCellValue);
const selectCell = useSpreadsheetStore((s) => s.selectCell);
const isSelected = selectedCell === cellId;
return (
<td
className={isSelected ? "border-blue-500 border-2" : "border"}
onClick={() => selectCell(cellId)}
>
{isSelected ? (
<input
autoFocus
value={cell?.value ?? ""}
onChange={(e) => setCellValue(cellId, e.target.value)}
style={{
fontWeight: cell?.format.bold ? "bold" : "normal",
fontStyle: cell?.format.italic ? "italic" : "normal",
color: cell?.format.color,
}}
/>
) : (
<span>{cell?.value ?? ""}</span>
)}
</td>
);
}immer envuelve la función set. Cuando llamas a set((state) => { ... }), immer crea un draft proxy del state actual.push, splice, delete, assign). Immer rastrea todos los cambios y produce un nuevo objeto de state inmutable.set retorna, immer finaliza el draft en un nuevo state congelado y Zustand provoca un re-renderizado.Sin immer (spread manual):
// Esto es lo que escribirías sin immer
formatCell: (cellId, format) =>
set((state) => ({
cells: {
...state.cells,
[cellId]: {
...state.cells[cellId],
format: {
...state.cells[cellId].format,
...format,
},
},
},
})),Immer con persist:
create<Store>()(
persist(
immer((set) => ({ ... })),
{ name: "store" }
)
);Devolver un nuevo state en lugar de mutar:
// También puedes devolver un nuevo valor desde set con immer
set((state) => {
// Devolver reemplaza el state (igual que sin immer)
return { count: state.count + 1 };
});set, el parámetro state está tipado como Draft<State>, lo que hace que todas las propiedades sean mutables.create<State>()(immer(...)) con doble invocación para una inferencia de tipos correcta.import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
// Los tipos funcionan igual con o sin immer
export const useStore = create<MyState>()(
immer((set) => ({
nested: { deep: { value: 0 } },
update: () =>
set((state) => {
state.nested.deep.value = 42; // Completamente tipado
}),
}))
);immer debe ser el middleware más interno (el más cercano al creador del store). Colocarlo fuera de persist o devtools no funcionará correctamente.set. O mutas o devuelves, no ambas cosas. Immer lanzará un error si haces las dos.enableMapSet(). Llámalo una vez en el punto de entrada de tu app si lo necesitas.state dentro de set es un draft Proxy. No lo pases a funciones async ni lo guardes para usarlo después. Se vuelve inválido después de que set retorna.Object.keys() y for...in funcionan en drafts, pero algunos casos límite con Array.isArray() o comprobaciones instanceof pueden comportarse de forma inesperada en draft proxies.| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Middleware de Immer | Actualizaciones anidadas limpias, sintaxis familiar | Tamaño del bundle, sobrecarga de Proxy |
| Spread manual | Sin dependencias, control total | Verboso para state profundamente anidado |
| structuredClone + mutate | No requiere librería | Clona todo el state, sin structural sharing |
| Diseño de state plano | Sin anidamiento profundo que gestionar | Puede requerir normalización |
state.count += 1) que produce state inmutable por debajo.npm install immer. El middleware immer de Zustand importa desde zustand/middleware/immer pero depende del paquete immer.push, splice, delete, assign).set retorna, immer finaliza el draft en un nuevo objeto de state congelado.devtools(persist(immer((set) => ({ ... })))).set retorna.set.enableMapSet() una vez en el punto de entrada de tu app.state está tipado como Draft<State>, lo que hace que todas las propiedades sean mutables.export const useStore = create<MyState>()(
immer((set) => ({
nested: { deep: { value: 0 } },
update: () => set((state) => {
state.nested.deep.value = 42; // Completamente tipado
}),
}))
);create<State>()(immer(...)) para una inferencia de TypeScript correcta.// Sin immer (spread manual)
formatCell: (cellId, format) =>
set((state) => ({
cells: {
...state.cells,
[cellId]: {
...state.cells[cellId],
format: { ...state.cells[cellId].format, ...format },
},
},
})),
// Con immer
formatCell: (cellId, format) =>
set((state) => {
Object.assign(state.cells[cellId].format, format);
}),Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥