Atualizar estado no React sem mutar array e objeto
Por que push e atribuição direta não renderizam, como copiar com spread, map e filter, e o que muda quando o estado tem objeto aninhado.
Você clica em “Receber pedido”, o console informa três pedidos, mas a tela ainda mostra dois. O dado mudou; a interface não. Esse desencontro costuma nascer de uma mutação: o código alterou o array que o React já conhecia.
Nesta aula, você vai reproduzir o bug, entender por que ele acontece e corrigir
arrays e objetos sem perder dados. Começaremos com um push e chegaremos a um
painel com inclusão, remoção, status, objetos aninhados e componentes filhos.
Estado é a memória que o React preserva entre renderizações. O setter é a
função, como setPedidos, usada para solicitar sua atualização. Uma
referência identifica onde um array ou objeto está na memória. Mutação é
alterar esse valor existente; imutabilidade é criar o próximo valor sem
modificar o anterior.
O porquê vem antes das receitas: ao receber a mesma referência, o React pode
concluir que o estado continua igual e pular a atualização. Por isso push,
sort e uma atribuição direta podem mudar o conteúdo sem atualizar a tela.
Os exemplos usam o painel da Bom Burger, uma hamburgueria fictícia criada
apenas para esta aula. A lista fica no mesmo useState explicado em useState
no React.
Por que o React precisa de uma nova referência?
Porque a nova referência sinaliza que o estado foi substituído. Alterar apenas o conteúdo mantém o mesmo identificador e pode fazer React ignorar a mudança.
Imagine caixas de pedidos no balcão. Conteúdo da caixa é o dado, caixa é o
array ou objeto, etiqueta é a referência e o atendente que compara etiquetas é
Object.is, uma comparação que informa se dois valores são iguais. Trocar o
conteúdo sem trocar a etiqueta é mutar. Spread copia itens, map transforma
itens e filter seleciona itens; os três montam outra caixa. O limite da
analogia: referências não são etiquetas visíveis, e React ainda pode renderizar
por outros motivos; ela explica apenas esta comparação de estado.
Experimente você mesmo: o array muda e a tela acompanha?
No primeiro exemplo, observe a lista antes de clicar e tente prever três resultados: o tamanho do array, quantos nomes continuarão visíveis e quantas vezes o componente será executado. Clique uma vez e só então confira a saída.
Este é o componente que quase todo mundo escreve na primeira vez. O botão
adiciona um pedido com push e devolve a mesma lista para o setter.
function PainelDaCozinha() {
const [pedidos, setPedidos] = useState([
{ id: 41, cliente: 'Ana', item: 'X-Salada' },
{ id: 42, cliente: 'Bruno', item: 'X-Bacon' },
]);
function receberPedido() {
pedidos.push({ id: 43, cliente: 'Carla', item: 'X-Tudo' });
setPedidos(pedidos);
console.log('pedidos no array:', pedidos.length);
}
return (
<div>
<ul>
{pedidos.map((pedido) => (
<li key={pedido.id}>{pedido.cliente} </li>
))}
</ul>
<button onClick={receberPedido}>Receber pedido</button>
</div>
);
}useState devolve o par pedidos, valor atual, e setPedidos, setter. Cada
execução de PainelDaCozinha é uma renderização: o map descreve uma linha
por pedido, key identifica essa linha e onClick entrega receberPedido ao
botão. No clique, push acrescenta Carla ao mesmo array e o setter recebe essa
referência antiga.
Montei o componente no Node com jsdom, que simula o DOM, a árvore de elementos do navegador. Depois do clique, medi o array, a tela e a quantidade de execuções do componente.
O array tem três pedidos. A tela mostra dois. E o número que explica tudo é o
último: o componente rodou uma vez só, a montagem. O setPedidos foi
chamado e o React simplesmente ignorou.
Trocando o push por uma cópia com spread, e sem mudar mais nada:
function receberPedido() {
setPedidos([...pedidos, { id: 43, cliente: 'Carla', item: 'X-Tudo' }]);
}O spread ...pedidos copia os itens para um array novo; o objeto depois da
vírgula entra no fim. A nova caixa fez o componente executar outra vez: duas
renderizações e três nomes na tela.
Como o React decide se o estado mudou?
O React compara o estado anterior e o próximo com Object.is. Essa função retorna
true quando os dois argumentos representam o mesmo valor; para arrays e
objetos, isso exige a mesma referência.
Depois de setPedidos, se Object.is(estadoAntigo, estadoNovo) der true, o
React pode pular a renderização. No primeiro teste, a função do componente nem foi
chamada novamente.
Quando o estado é um número ou um texto, Object.is compara o valor e tudo
acontece como você espera: 2 é diferente de 3 e a tela atualiza. Com array e
objeto ele compara a caixa, não o que está guardado dentro dela. É por isso
que push te derruba: a caixa continua sendo a mesma.
Experimente você mesmo: quais métodos mantêm a referência?
Métodos que mutam o array mantêm a referência; métodos que criam uma cópia não.
Antes de ler o resultado, marque quais das doze operações você espera que deem
Object.is: true. Depois confira sua regra na tabela executada.
const pedidos = [
{ id: 41, cliente: 'Ana', item: 'X-Salada' },
{ id: 42, cliente: 'Bruno', item: 'X-Bacon' },
];
const novo = { id: 43, cliente: 'Carla', item: 'X-Tudo' };
const porCliente = (a, b) => a.cliente.localeCompare(b.cliente);
// push e splice nao devolvem o array: devolvem o tamanho novo e os removidos.
// Nas duas primeiras linhas o `, p` no fim e de proposito, para comparar
// array com array e nao array com numero.
const operacoes = {
'push(novo)': (p) => (p.push(novo), p),
'splice(0, 1)': (p) => (p.splice(0, 1), p),
'sort(porCliente)': (p) => p.sort(porCliente),
'reverse()': (p) => p.reverse(),
'[...p, novo]': (p) => [...p, novo],
'concat(novo)': (p) => p.concat(novo),
'filter(...)': (p) => p.filter((x) => x.id !== 41),
'map(...)': (p) => p.map((x) => x),
'slice(1)': (p) => p.slice(1),
'toSorted(porCliente)': (p) => p.toSorted(porCliente),
'toSpliced(0, 1)': (p) => p.toSpliced(0, 1),
'with(0, novo)': (p) => p.with(0, novo),
};
for (const [rotulo, operacao] of Object.entries(operacoes)) {
const antes = pedidos.slice();
const depois = operacao(antes);
const mesmaRef = Object.is(antes, depois);
console.log(
rotulo.padEnd(21),
'| Object.is:',
String(mesmaRef).padEnd(5),
'| React repinta:',
mesmaRef ? 'nao' : 'sim',
);
}Object.entries percorre cada rótulo e operação; slice prepara uma lista limpa
para o teste seguinte; Object.is compara entrada e saída. padEnd apenas
alinha as colunas impressas.
Essa lista é a lição inteira em quatro linhas de true. Os quatro métodos que
dão true — push, splice, sort e reverse — são exatamente os que mexem
no array original: depois deles, a variável que entrou e a que saiu são a mesma
caixa. Nenhum dos quatro serve para atualizar estado. Todos os outros montam um
array novo, e é por isso que funcionam.
Até aqui: mutar troca o conteúdo da caixa, mas preserva sua referência.
Criar outro array faz Object.is retornar false e sinaliza o novo estado.
Como adicionar, remover e atualizar itens sem mutar?
Use spread para adicionar, filter para remover e map para substituir apenas
o item desejado. Os três criam um array novo sem alterar a lista anterior.
As operações do painel viram quatro funções com os mesmos métodos de map, filter e reduce, agora com um destino diferente.
// tudo isto vive dentro do PainelDaCozinha, ao lado do useState de pedidos
const receber = () =>
setPedidos([...pedidos, { id: 44, cliente: 'Diego', status: 'na fila' }]);
const urgente = () =>
setPedidos([{ id: 45, cliente: 'Eva', status: 'na chapa' }, ...pedidos]);
const cancelar = (id) => setPedidos(pedidos.filter((pedido) => pedido.id !== id));
const mudarStatus = (id, status) =>
setPedidos(pedidos.map((pedido) => (pedido.id === id ? { ...pedido, status } : pedido)));Cliquei nos quatro botões em sequência, imprimindo a lista da tela depois de cada clique:
receber espalha a lista e acrescenta Diego; urgente coloca Eva antes do
spread. filter mantém apenas pedidos com outro id, removendo Bruno. Por fim,
map cria um objeto novo para o pedido encontrado e reaproveita os demais.
O React usa a prop key para casar cada item com a linha existente, como explica
listas e key.
Quando usar a função atualizadora do setter?
Use-a quando o próximo estado depende do anterior. O setter recebe uma
callback, função que o React chamará com o valor mais recente da fila; isso
evita trabalhar com pedidos congelado na renderização atual.
function receberDoisDeUmaVez() {
setPedidos([...pedidos, { id: 42, cliente: 'Bruno' }]);
setPedidos([...pedidos, { id: 43, cliente: 'Carla' }]);
}
function receberDoisComFuncao() {
setPedidos((anteriores) => [...anteriores, { id: 42, cliente: 'Bruno' }]);
setPedidos((anteriores) => [...anteriores, { id: 43, cliente: 'Carla' }]);
}Na primeira função, as duas chamadas copiam a mesma lista e a segunda substitui a primeira; por isso Bruno some. Na versão com callback, cada chamada recebe o resultado da anterior e os dois pedidos entram.
Por que setPedidos com push quebra o componente?
Porque push devolve o novo tamanho do array, não o array atualizado. O setter
guarda um número e, na renderização seguinte, pedidos.map deixa de existir.
Descoberto que push não funciona sozinho, muita gente tenta o atalho: passar o
push direto para o setter.
function receberPedido() {
setPedidos(pedidos.push({ id: 42, cliente: 'Bruno' }));
}TypeError: pedidos.map is not a function at PainelDaCozinha (/private/tmp/burger/08-erro.jsx:14:18) at Object.react_stack_bottom_frame (/private/tmp/burger/node_modules/react-dom/cjs/react-dom-client.development.js:25904:20) at renderWithHooks (/private/tmp/burger/node_modules/react-dom/cjs/react-dom-client.development.js:7662:22) at updateFunctionComponent (/private/tmp/burger/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19)
Node.js v24.16.0
TypeError indica uma operação incompatível com o tipo do valor: o estado virou
o número 2, que não possui map. A mensagem aponta para a lista porque o erro
aparece na renderização seguinte, embora o clique anterior o tenha causado.
Como atualizar um objeto dentro de um array?
Crie um array com map e um objeto novo apenas para o item alterado. Copiar o
array e mutar o objeto interno preserva uma referência que ainda pode esconder a
mudança.
Para tornar o problema visível, Comanda usa memo, função que permite ao React
reaproveitar o componente quando suas props, os dados recebidos pelo filho,
continuam iguais. Compare mutar o pedido com copiá-lo:
const Comanda = memo(function Comanda({ pedido }) {
return <li>{pedido.cliente}: {pedido.status} </li>;
});
function mutando() {
pedidos[0].status = 'na chapa';
setPedidos([...pedidos]);
}
function copiando() {
setPedidos(pedidos.map((p) => (p.id === 41 ? { ...p, status: 'na chapa' } : p)));
}O array novo fez o pai renderizar, mas Ana ficou com “na fila” porque pedido
ainda era o mesmo objeto. O mecanismo é detalhado em useMemo e
useCallback. Com map e spread, apenas o
pedido 41 ganha nova referência, e a linha atualiza.
Esse é o motivo prático de não mutar mesmo quando “parece que funciona”: o código passa a depender de nenhum filho ser memorizado, de nenhuma comparação existir no caminho. O dia em que alguém otimizar um componente, a tela para de atualizar por um motivo que parece distante da mutação original.
Até aqui: a lista precisa de outra referência, e o objeto alterado também. Os itens intactos podem ser reaproveitados para preservar trabalho e identidade.
Como atualizar um objeto aninhado sem mutar?
Copie cada nível entre a raiz do estado e o campo alterado. O spread faz uma cópia rasa: duplica apenas o recipiente atual, não os objetos guardados nele.
Na primeira tentativa, spread cria outro pedido, mas os dois pedidos ainda
compartilham o mesmo objeto entrega:
const pedido = {
id: 41,
cliente: 'Ana',
entrega: { bairro: 'Santana', rua: 'Voluntarios da Patria, 120' },
};
const rasa = { ...pedido };
rasa.entrega.rua = 'Alfredo Pujol, 88';
console.log('pedido e rasa sao o mesmo objeto? ', Object.is(pedido, rasa));
console.log('entrega e o mesmo objeto? ', Object.is(pedido.entrega, rasa.entrega));
console.log('rua no pedido original: ', pedido.entrega.rua);A terceira linha é o estrago: o pedido original, que deveria estar intocado, mudou de rua. Se você guardava esse objeto para desfazer a edição, o “antes” já não existe mais.
A regra é copiar todo o caminho entre a raiz do estado e o campo que muda.
O bloco abaixo é um arquivo novo — o pedido volta a nascer com a rua original,
porque o de cima já foi estragado:
const pedido = {
id: 41,
cliente: 'Ana',
entrega: { bairro: 'Santana', rua: 'Voluntarios da Patria, 120' },
};
const novo = {
...pedido,
entrega: { ...pedido.entrega, rua: 'Alfredo Pujol, 88' },
};
console.log('entrega e o mesmo objeto? ', Object.is(pedido.entrega, novo.entrega));
console.log('rua no original: ', pedido.entrega.rua);
console.log('rua no novo: ', novo.entrega.rua);
console.log('bairro veio junto? ', novo.entrega.bairro);O bairro sobreviveu sem você escrever o nome dele: o spread copia os campos
que não foram sobrescritos. Vale revisar
desestruturação, spread e rest
se essa parte ainda parece mágica.
Quais métodos de array devolvem uma cópia?
toSorted, toReversed, toSpliced e with são alternativas imutáveis para
ordenar, inverter, remover/inserir e substituir por índice. Todos devolvem um
array novo em vez de alterar o original.
const porCliente = (a, b) => a.cliente.localeCompare(b.cliente);
// os dois botões vivem dentro do PainelDaCozinha, com o mesmo useState de sempre
<button id="sort" onClick={() => setPedidos(pedidos.sort(porCliente))}>1</button>
<button id="tosorted" onClick={() => setPedidos(pedidos.toSorted(porCliente))}>2</button>Cliquei no primeiro botão e depois no segundo, começando com Carla, Ana e Bruno na fila:
O detalhe que confunde: depois do primeiro clique o array já estava ordenado em memória. A tela só ficou sabendo no segundo clique, quando um array novo apareceu. Um bug assim se disfarça de “às vezes funciona” — é o mesmo assunto de por que o componente renderiza de novo.
Para trocar o item de uma posição conhecida, with substitui o velho
pedidos[2] = novo sem mutar:
const pedidos = [
{ id: 41, cliente: 'Ana', status: 'na fila' },
{ id: 42, cliente: 'Bruno', status: 'na fila' },
{ id: 43, cliente: 'Carla', status: 'na chapa' },
];
const atualizados = pedidos.with(2, { ...pedidos[2], status: 'entregue' });
console.log('array novo? ', !Object.is(pedidos, atualizados));
console.log('status no original: ', pedidos[2].status);
console.log('status no novo: ', atualizados[2].status);
console.log('pedido 41 preservado?', Object.is(pedidos[0], atualizados[0]));A última linha separa with de structuredClone, que copia profundamente toda
a estrutura. Com with, o pedido intacto preserva sua referência e um filho
memorizado pode evitar outra renderização.
Quando vale achatar o estado?
Quando uma pequena alteração exige uma escada de spreads, separe o estado em partes mais rasas. Cada setter passa a cuidar apenas dos dados que mudam juntos.
O mesmo mudarStatus agora está num estado com três níveis. Trocar uma palavra
exige três spreads até a cozinha e outro para o pedido:
const fundo = {
loja: {
nome: 'Bom Burger',
cozinha: {
chapaLigada: true,
pedidos: [
{ id: 41, cliente: 'Ana', status: 'na fila' },
{ id: 42, cliente: 'Bruno', status: 'na fila' },
],
},
},
};
const atualizado = {
...fundo,
loja: {
...fundo.loja,
cozinha: {
...fundo.loja.cozinha,
pedidos: fundo.loja.cozinha.pedidos.map((p) =>
p.id === 41 ? { ...p, status: 'na chapa' } : p,
),
},
},
};
const pedidosAntes = fundo.loja.cozinha.pedidos;
const pedidosDepois = atualizado.loja.cozinha.pedidos;
console.log('status no original: ', pedidosAntes[0].status);
console.log('status no novo: ', pedidosDepois[0].status);
console.log('chapaLigada veio junto?', atualizado.loja.cozinha.chapaLigada);
console.log('Bruno preservado? ', Object.is(pedidosAntes[1], pedidosDepois[1]));Funciona, e funciona bem: o original ficou intocado, a chapaLigada veio junto
sem você digitar o nome dela e o Bruno continua sendo o mesmo objeto. Mas cada
nível é uma chance de esquecer um spread e mutar o original sem perceber — e
ninguém revisa esse bloco com atenção na segunda vez que ele aparece. Quase
sempre a resposta não é copiar melhor, é guardar mais raso: três useState
independentes no lugar de um objeto com três andares.
const [nomeDaLoja] = useState('Bom Burger');
const [chapaLigada, setChapaLigada] = useState(true);
const [pedidos, setPedidos] = useState([
{ id: 41, cliente: 'Ana', status: 'na fila' },
{ id: 42, cliente: 'Bruno', status: 'na fila' },
]);
const mudarStatus = (id, status) =>
setPedidos(pedidos.map((p) => (p.id === id ? { ...p, status } : p)));O primeiro clique mudou só o pedido, o segundo mudou só a chapa, e nenhum dos
dois precisou saber da existência do outro. Cada pedaço do estado muda sozinho,
e a cópia nunca passa de um nível. Quando os pedaços realmente andam juntos e as
transições ficam muitas, mais adiante na trilha aparece o useReducer, que
junta regras de atualização numa função central — mas as cópias continuam sendo
exatamente estas.
O que aprender depois de estado imutável?
A lição 10 aplica a mesma regra a cada tecla digitada: formulário controlado
no React. Nela, onChange, o evento
disparado quando o campo muda, usa o setter para produzir o próximo valor.
Até aqui: você viu por que a referência importa, trocou métodos mutadores por
spread, map, filter e métodos de cópia, atualizou objetos aninhados e aprendeu
quando achatar o estado.
Leve esta regra: não altere diretamente um valor que está dentro do estado. Crie o próximo valor e entregue-o ao setter. A trilha de React mostra a sequência completa.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
Por que o React não olha dentro do array para saber se mudou?
Preciso instalar Immer ou alguma biblioteca para isso?
E o structuredClone, não resolve tudo de uma vez?
Quando devo passar uma função para o setState em vez do valor?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0 com React 19.2.8 e jsdom 30.0.1, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- React — Updating Arrays in State — react.dev
- React — Updating Objects in State — react.dev
- MDN — Object.is() — developer.mozilla.org
- MDN — Array.prototype.toSorted() — developer.mozilla.org



