Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

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.

Rodolfo Mori12 min de leitura

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.

jsx
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.

pedidos no array: 3 pedidos na tela: Ana Bruno renderizacoes: 1

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:

jsx
function receberPedido() {
  setPedidos([...pedidos, { id: 43, cliente: 'Carla', item: 'X-Tudo' }]);
}
pedidos na tela: Ana Bruno Carla renderizacoes: 2

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.

pedidos.push(novo) — Object.is = true antes depois [41] [42] [43] nao repinta [...pedidos, novo] — Object.is = false antes depois [41] [42] [41] [42] [43] repinta

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.

js
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',
  );
}
push(novo) | Object.is: true | React repinta: nao splice(0, 1) | Object.is: true | React repinta: nao sort(porCliente) | Object.is: true | React repinta: nao reverse() | Object.is: true | React repinta: nao [...p, novo] | Object.is: false | React repinta: sim concat(novo) | Object.is: false | React repinta: sim filter(...) | Object.is: false | React repinta: sim map(...) | Object.is: false | React repinta: sim slice(1) | Object.is: false | React repinta: sim toSorted(porCliente) | Object.is: false | React repinta: sim toSpliced(0, 1) | Object.is: false | React repinta: sim with(0, novo) | Object.is: false | React repinta: 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 truepush, 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.

jsx
// 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:

inicial: Ana(na fila) Bruno(na fila) Carla(na fila) recebeu: Ana(na fila) Bruno(na fila) Carla(na fila) Diego(na fila) urgencia: Eva(na chapa) Ana(na fila) Bruno(na fila) Carla(na fila) Diego(na fila) cancelou: Eva(na chapa) Ana(na fila) Carla(na fila) Diego(na fila) na chapa: Eva(na chapa) Ana(na chapa) Carla(na fila) Diego(na fila)

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.

jsx
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' }]);
}
passando o valor: Ana Carla passando a funcao: Ana Bruno 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.

jsx
function receberPedido() {
  setPedidos(pedidos.push({ id: 42, cliente: 'Bruno' }));
}
/private/tmp/burger/08-erro.jsx:14 {pedidos.map((pedido) => ( ^

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:

jsx
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)));
}
depois de mutar o objeto: Ana: na fila Bruno: na fila depois de copiar o objeto: Ana: na chapa Bruno: na fila

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:

js
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);
pedido e rasa sao o mesmo objeto? false entrega e o mesmo objeto? true rua no pedido original: Alfredo Pujol, 88

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:

js
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);
entrega e o mesmo objeto? false rua no original: Voluntarios da Patria, 120 rua no novo: Alfredo Pujol, 88 bairro veio junto? Santana

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.

jsx
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:

inicial: Carla Ana Bruno sort: Carla Ana Bruno toSorted: Ana Bruno Carla

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:

js
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]));
array novo? true status no original: na chapa status no novo: entregue pedido 41 preservado? true

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:

js
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]));
status no original: na fila status no novo: na chapa chapaLigada veio junto? true Bruno preservado? true

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.

jsx
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)));
Bom Burger — chapa ligada | Ana(na chapa) Bruno(na fila) Bom Burger — chapa desligada | Ana(na chapa) Bruno(na fila)

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.

Ver todos os vídeos do canal
  • react
  • estado
  • imutabilidade
  • spread
  • arrays

Perguntas frequentes

Por que o React não olha dentro do array para saber se mudou?
Porque olhar dentro custa caro. Comparar duas referências é uma instrução de máquina; comparar duas listas de mil pedidos, campo por campo, é mil comparações a cada clique. O React trocou trabalho de comparação por uma regra que você precisa seguir: estado novo, objeto novo.
Preciso instalar Immer ou alguma biblioteca para isso?
Não para começar. Spread, map e filter resolvem a maioria dos casos, e os métodos toSorted, toSpliced e with cobrem boa parte do resto. O Immer só passa a compensar quando o estado é profundo de verdade e as cópias em cadeia começam a virar linhas ilegíveis.
E o structuredClone, não resolve tudo de uma vez?
Resolve o problema da referência, mas copia o objeto inteiro a cada atualização, joga fora a identidade de todos os itens que não mudaram e quebra em valores como função e classe. Copiar só o caminho que muda é mais rápido e preserva os itens intactos.
Quando devo passar uma função para o setState em vez do valor?
Sempre que o novo estado depende do anterior e você pode chamar o setter mais de uma vez no mesmo evento, ou dentro de setTimeout, promise e handlers de socket. A forma com função lê o valor mais recente da fila em vez do valor congelado naquela renderização.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. React — Updating Arrays in State — react.dev
  2. React — Updating Objects in State — react.dev
  3. MDN — Object.is() — developer.mozilla.org
  4. MDN — Array.prototype.toSorted() — developer.mozilla.org

Continue por aqui