Estado Imutável de Forma Simples: Entendendo o Immer
Atualizar estado aninhado em JavaScript sem mutação é tedioso. Você espalha objetos em cada nível, rastreia quais referências mudaram e torce para não ter mutado algo acidentalmente pelo caminho. Para uma simples atualização aninhada, você pode escrever cinco linhas de lógica cuidadosa de cópia.
Immer resolve este problema com uma abordagem diferente: escreva código que parece mutação, mas produz estado imutável automaticamente. Este artigo explica como funciona a imutabilidade baseada em proxy do Immer, por que o Redux Toolkit o usa internamente, e quais ressalvas práticas você deve conhecer antes de adotá-lo.
Pontos-Chave
- O Immer permite escrever código no estilo de mutação que produz estado imutável através de sua função
producee drafts baseados em proxy - O compartilhamento estrutural garante que apenas os ramos modificados recebam novas referências, preservando o desempenho para verificações de re-renderização do React e Redux
- O Redux Toolkit usa o Immer internamente, então os reducers do
createSlicelidam automaticamente com imutabilidade sem importações extras - Atenção para armadilhas comuns: não reatribua o draft em si, evite misturar mutações de draft com valores de retorno, e tenha cuidado com instâncias de classes
Como o Immer Produz Estado Imutável
A API principal do Immer é a função produce. Você passa para ela seu estado atual e uma função “receita”. Dentro dessa receita, você recebe um draft—um proxy envolvendo seu estado original. Você modifica o draft usando mutações JavaScript normais. Quando a receita termina, o Immer gera um novo estado imutável refletindo suas mudanças.
import { produce } from "immer"
const baseState = {
user: { name: "Alice", settings: { theme: "dark" } }
}
const nextState = produce(baseState, draft => {
draft.user.settings.theme = "light"
})
// baseState.user.settings.theme ainda é "dark"
// nextState.user.settings.theme é "light"
O modelo mental é direto: finja que está mutando, mas o Immer cuida das atualizações imutáveis nos bastidores.
Imutabilidade Baseada em Proxy e Compartilhamento Estrutural
O Immer usa objetos Proxy do JavaScript para interceptar suas leituras e escritas no draft. Quando você acessa uma propriedade aninhada, o Immer cria preguiçosamente um proxy para aquele caminho. Quando você escreve em uma propriedade, o Immer marca aquele nó (e seus ancestrais) como modificado e cria cópias rasas apenas onde necessário.
Esta abordagem de cópia-na-escrita habilita o compartilhamento estrutural. Porções inalteradas da sua árvore de estado permanecem com as mesmas referências de objeto. Apenas ramos modificados recebem novas referências. Isto importa para React e Redux porque verificações de igualdade de referência determinam se componentes re-renderizam.
O Immer moderno (v10+) requer suporte nativo a Proxy—não há fallback para ES5. Isto é adequado para navegadores atuais e Node.js, mas vale notar se você tem como alvo ambientes incomuns.
Integração do Immer com Redux Toolkit
Se você usa Redux Toolkit, você já está usando o Immer. O createSlice do RTK envolve sua lógica de reducer com produce automaticamente. Você escreve código “mutante” nos case reducers, e o RTK cuida da imutabilidade:
import { createSlice } from "@reduxjs/toolkit"
const todosSlice = createSlice({
name: "todos",
initialState: [],
reducers: {
addTodo: (state, action) => {
state.push(action.payload) // Parece mutação, mas é seguro
},
toggleTodo: (state, action) => {
const todo = state.find(t => t.id === action.payload)
if (todo) todo.completed = !todo.completed
}
}
})
Esta integração do Immer com Redux Toolkit elimina o boilerplate do operador spread que tornava os reducers vanilla Redux verbosos. Você não precisa importar produce separadamente—o RTK o configura para você.
Discover how at OpenReplay.com.
Auto-Congelamento e Comportamento de Iteração
O Immer congela automaticamente o estado produzido por padrão (usando Object.freeze). Isto captura mutações acidentais durante o desenvolvimento, mas adiciona sobrecarga. Você pode desabilitá-lo em produção via configuração se o profiling mostrar que isso importa:
import { setAutoFreeze } from "immer"
setAutoFreeze(false) // Desabilitar em produção para desempenho
O Immer moderno usa por padrão iteração solta para desempenho: apenas chaves string enumeráveis são iteradas em drafts. Isto é geralmente o que você quer para estado de aplicação, mas significa que chaves symbol e propriedades não-enumeráveis são puladas a menos que você habilite explicitamente iteração estrita. Isto importa principalmente em casos extremos ou manipulação de dados de baixo nível.
Ressalvas Práticas para Atualizações Imutáveis em JavaScript
Várias armadilhas pegam desenvolvedores novos no Immer:
Não reatribua o draft em si. Modificar draft.property funciona. Reatribuir draft = newValue não faz nada útil porque você está apenas mudando a vinculação do parâmetro local.
Retornar valores importa. Se sua receita retorna um valor, o Immer usa aquilo como o novo estado ao invés do draft modificado. Retornar undefined é tratado da mesma forma que não retornar nada, mas misturar mutações de draft e retornos explícitos é uma fonte comum de bugs—use uma abordagem ou outra.
Classes e objetos exóticos precisam de cuidado. O Immer funciona melhor com objetos simples, arrays, Maps e Sets. Instâncias de classes não são automaticamente proxiadas corretamente. Você pode precisar marcá-las como imutáveis ou lidar com elas separadamente (conforme descrito na documentação oficial de armadilhas).
Desempenho não é gratuito. O Immer é adequado para estado típico de UI—formulários, listas, preferências de usuário. Para loops intensos processando milhares de itens por frame, meça antes de assumir que o Immer não adiciona sobrecarga. A maquinaria de proxy tem um custo.
Estado em forma de árvore funciona melhor. O Immer assume que seu estado é uma árvore. Referências circulares ou referências de objetos compartilhadas entre ramos podem produzir resultados inesperados.
Conclusão
O Immer brilha para atualizações de estado aninhado moderadamente complexas—exatamente o que você encontra em componentes React e reducers Redux. Ele elimina boilerplate, captura mutações acidentais e integra-se perfeitamente com Redux Toolkit.
Para estado simples e plano, operadores spread nativos funcionam bem. Para processamento de dados crítico em desempenho, faça benchmark primeiro. Mas para gerenciamento de estado frontend típico, a abordagem baseada em proxy do Immer oferece o melhor equilíbrio entre experiência do desenvolvedor e correção.
Perguntas Frequentes
Sim. Você pode envolver suas atualizações de estado com produce diretamente, ou usar o hook useImmer do Immer do pacote use-immer. Isto lhe dá a mesma sintaxe no estilo de mutação para estado local de componente que o Redux Toolkit fornece para estado global.
O Immer tem excelente suporte a TypeScript. A função produce infere tipos automaticamente do seu estado base, e objetos draft mantêm tipagem adequada. Você obtém autocomplete completo e verificação de tipos enquanto escreve código no estilo de mutação.
O Immer suporta Maps e Sets nativamente. Você pode usar métodos padrão de Map e Set como set, delete e add diretamente em drafts em versões modernas sem qualquer configuração adicional.
Apenas se o profiling mostrar que causa problemas de desempenho. O auto-freeze ajuda a capturar bugs lançando erros em mutações acidentais. Para a maioria das aplicações a sobrecarga é negligenciável, mas atualizações de alta frequência podem se beneficiar de desabilitá-lo via setAutoFreeze false.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.