//
Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Use o middleware immer para escrever atualizações de estado em estilo mutável que produzem estado imutável nos bastidores. Isso elimina a propagação manual de objetos para atualizações aninhadas.
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 envolve a função set. Quando você chama set((state) => { ... }), o Immer cria um proxy de rascunho do estado atual.set, o Immer finaliza o rascunho em um novo estado congelado e o Zustand aciona uma nova renderização.Sem Immer (propagação manual):
// Isso é o que você escreveria sem immer
formatCell: (cellId, format) =>
set((state) => ({
cells: {
...state.cells,
[cellId]: {
...state.cells[cellId],
format: {
...state.cells[cellId].format,
...format,
},
},
},
})),Immer com persist:
create<Store>()(
persist(
immer((set) => ({ ... })),
{ name: "store" }
)
);Retornando novo estado em vez de mutar:
// Você também pode retornar um novo valor de set com immer
set((state) => {
// Retornar substitui o estado (o mesmo que sem immer)
return { count: state.count + 1 };
});set, o parâmetro state é tipado como Draft<State>, o que torna todas as propriedades mutáveis.create<State>()(immer(...)) com invocação dupla para inferência de tipo correta.import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
// Os tipos funcionam de forma idêntica com ou sem immer
export const useStore = create<MyState>()(
immer((set) => ({
nested: { deep: { value: 0 } },
update: () =>
set((state) => {
state.nested.deep.value = 42; // Totalmente tipado
}),
}))
);immer deve ser o middleware mais interno (mais próximo do criador do store). Colocá-lo fora de persist ou devtools não funcionará corretamente.set. Ou modifique ou retorne, não ambos. O Immer lançará um erro se você fizer ambos.enableMapSet(). Chame-o uma vez no ponto de entrada do seu aplicativo, se necessário.state dentro de set é um proxy draft. Não o passe para funções assíncronas ou armazene-o para uso posterior. Ele se torna inválido após o retorno de set.Object.keys() e for...in funcionam em rascunhos, mas alguns casos extremos com verificações Array.isArray() ou instanceof podem se comportar inesperadamente em proxies de rascunho.| Abordagem | Prós | Contras |
|---|---|---|
| Middleware Immer | Atualizações aninhadas limpas, sintaxe familiar | Tamanho do bundle, sobrecarga do Proxy |
| Propagação manual | Sem dependências, controle total | Verboso para estado profundamente aninhado |
| structuredClone + mutate | Nenhuma biblioteca necessária | Clona todo o estado, sem compartilhamento estrutural |
| Design de estado plano | Sem aninhamento profundo para se preocupar | Pode exigir normalização |
state.count += 1) que produz estado imutável nos bastidores.npm install immer. O middleware immer do Zustand importa de zustand/middleware/immer, mas depende do pacote immer.set, o Immer finaliza o rascunho em um novo objeto de estado congelado.devtools(persist(immer((set) => ({ ... })))).set.set.enableMapSet() uma vez no ponto de entrada do seu aplicativo.state é tipado como Draft<State>, o que torna todas as propriedades mutáveis.export const useStore = create<MyState>()(
immer((set) => ({
nested: { deep: { value: 0 } },
update: () => set((state) => {
state.nested.deep.value = 42; // Totalmente tipado
}),
}))
);create<State>()(immer(...)) para inferência correta de TypeScript.// Sem immer (propagação manual)
formatCell: (cellId, format) =>
set((state) => ({
cells: {
...state.cells,
[cellId]: {
...state.cells[cellId],
format: { ...state.cells[cellId].format, ...format },
},
},
})),
// Com immer
formatCell: (cellId, format) =>
set((state) => {
Object.assign(state.cells[cellId].format, format);
}),Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥