Menu

Atualizar arrays e objetos no estado do React (sem mutar)

Para mudar um array ou objeto no estado do React, crie um novo e passe-o para o setter. Aprenda a adicionar, remover, substituir, inserir e ordenar itens, atualizar objetos aninhados e por que mutar o estado deixa a tela sem mudança.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

No React você atualiza um array ou um objeto no estado criando um novo e passando-o para o setter. Nunca altere o valor existente no lugar: use spread (...), map, filter ou slice para montar uma cópia com a sua mudança e depois chame setItems(copy).

[...items, { ... }] é um novo array com um item a mais, e filter retorna um novo array sem o item removido. Os dois deixam o array antigo como estava.

Por que a mutação não renderiza

Quando você chama um setter, o React compara o novo valor com o antigo usando Object.is. Para arrays e objetos isso compara referências, não conteúdo. Se você altera um array com push e passa o mesmo array de volta, o React vê a mesma referência, conclui que nada mudou e pula a renderização.

Clique em "push" algumas vezes. O console mostra o array crescendo, mas nenhuma nova linha de render aparece e o texto continua a. Depois clique em "spread": acontece uma renderização e todos os itens adicionados com push aparecem de uma vez, porque estavam no array o tempo todo. A mutação não lança erro, ela só deixa a tela desatualizada até que outra coisa dispare uma renderização. Clique em "push" mais uma vez depois disso e desta vez pode ser que renderize: o React nem sempre consegue pular o trabalho de antemão, então uma mutação aparece algumas vezes e outras não. Essa imprevisibilidade é o motivo para não mutar o estado.

Arrays: métodos para usar e para evitar

A regra é simples: use métodos que retornam um novo array e evite métodos que alteram o array em que são chamados.

ObjetivoEvite (muta)Use (retorna um novo array)
Adicionarpush, unshift[...arr, item], [item, ...arr]
Removerpop, shift, splicefilter, slice
Substituirarr[i] = x, splicemap, with(i, x)
Inserirsplice[...arr.slice(0, i), x, ...arr.slice(i)], toSpliced
Ordenar, invertersort, reversetoSorted, toReversed ou copie antes

toSorted, toReversed, toSpliced e with são métodos de array mais novos (ES2023), suportados em todos os principais navegadores atuais desde 2023. Eles fazem o mesmo trabalho que seus primos que mutam, só que em uma cópia.

Atualizando um item com map

Para mudar um item, use map no array e retorne um novo objeto para o item que você quer e o objeto original para todos os outros.

A parte { ...todo, done: !todo.done } importa. Um bug comum é copiar o array mas alterar o objeto dentro dele:

const next = [...todos];
next[0].done = true; // next is new, but next[0] is the same object as todos[0]
setTodos(next);

Isso renderiza, então parece estar certo, mas o array antigo e o novo agora compartilham um objeto alterado. Qualquer coisa que compare o item antigo com o novo (um filho com React.memo, um efeito que depende do item) não vê diferença. Copie o item que você muda.

Inserindo em um índice

Não existe um insert que não mute, então corte o array em dois e coloque o novo item entre as metades. toSpliced(index, 0, item) faz o mesmo em uma chamada.

Defina o índice como 0 e insira de novo: o novo C vai para o início. Substitua a expressão de next por letters.toSpliced(index, 0, 'C') e o resultado é o mesmo.

Ordenando e invertendo

sort e reverse alteram o array em que são chamados, mesmo que também o retornem. Isso faz de setItems(items.sort()) uma mutação seguida de um set com a mesma referência, que é o bug do início desta página. Ordene uma cópia.

[...scores].reverse() funciona porque o spread faz a cópia primeiro e reverse então altera só essa cópia.

Atualizando objetos e objetos aninhados

Objetos seguem a mesma regra. Espalhe o objeto antigo em um novo e sobrescreva os campos que mudam. Para um objeto aninhado, faça o spread em cada nível no caminho até o campo: um único spread copia só o nível de cima, então o objeto interno continuaria sendo o antigo.

Cada nível que você copia é um novo objeto; cada nível em que você não mexe continua sendo o mesmo objeto. É isso que torna o spread barato: se o estado também guardasse uma longa lista de pedidos, { ...user } copiaria a referência da lista, não a lista em si.

Se um objeto no estado está aninhado em três ou quatro níveis e você continua escrevendo longas cadeias de spreads, isso muitas vezes é sinal de que o estado poderia ser mais plano. Guardar itens em um mapa por id, ou dividir um objeto grande em algumas chamadas de useState, mantém cada atualização curta. Quando muitos eventos mudam uma mesma estrutura, um reducer também ajuda (veja useReducer).

structuredClone vs spread

structuredClone(user) faz uma cópia profunda em uma chamada, e depois você pode alterar a cópia à vontade:

const next = structuredClone(user);
next.address.city = 'Oslo';
setUser(next);

Está correto, mas copia o objeto inteiro toda vez, incluindo as partes que você não mudou. Isso custa tempo em estados grandes e significa que todo objeto aninhado ganha uma nova referência, então um filho memoizado que recebe user.orders renderiza de novo mesmo que os pedidos não tenham mudado. O structuredClone também lança erro com funções, e instâncias de classe voltam como objetos comuns. O spread copia só o caminho que você muda, então é a escolha usual; deixe o structuredClone para a atualização rara que realmente reescreve a maior parte do objeto.

Escrevendo mutações com Immer

Immer é uma pequena biblioteca que permite escrever código que parece mutação. Ela entrega um rascunho, registra o que você muda e produz um novo valor imutável com só as partes alteradas copiadas. O pacote use-immer a envolve em um hook:

import { useImmer } from 'use-immer';

function Profile() {
    const [user, updateUser] = useImmer({
        name: 'Ada',
        address: { city: 'London' },
    });

    function moveTo(city) {
        updateUser((draft) => {
            draft.address.city = city;
        });
    }
    // ...
}

O Immer não está disponível nos editores desta página, que só carregam o React. Vale a pena adicioná-lo quando o seu estado é profundamente aninhado e os spreads ficam difíceis de ler. Para um estado plano, spread e map comuns bastam, e são o que você vai ver na maior parte do código React.

Perguntas frequentes

Como adiciono um item a um array no estado do React?

Crie um novo array que contenha os itens antigos mais o novo e passe-o para o setter: setItems([...items, newItem]). Não chame items.push(newItem), porque isso altera o array existente e o React vê a mesma referência.

Por que meu componente não renderiza de novo quando atualizo um array?

O mais provável é que você tenha alterado o array no lugar (push, splice, sort ou arr[i] = x) e passado o mesmo array de volta. O React compara o valor antigo e o novo com Object.is, encontra o mesmo objeto e pula a renderização. Passe um novo array.

Como atualizo um objeto dentro de um array no React?

Use map e retorne um novo objeto para o item que muda e o item original para todos os outros: setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t)).

Como atualizo um objeto aninhado no estado do React?

Copie cada nível do caminho até o campo que você muda: setUser({ ...user, address: { ...user.address, city: 'Oslo' } }). Um único spread copia só o nível de cima, então o objeto interno continuaria compartilhado.

Devo usar structuredClone ou o spread operator para o estado do React?

Prefira o spread. Ele copia só o que muda e mantém as partes intocadas como os mesmos objetos, o que ajuda o React.memo e as comparações. O structuredClone copia tudo toda vez, o que é mais simples de escrever, mas mais lento em estados grandes, e quebra a igualdade de referência das partes que não mudaram.

Ilustração das linguagens de programação do Coddy

Aprenda a programar com o Coddy

COMEÇAR