Atualizando Arrays no Estado

Arrays são mutáveis em JavaScript, mas você deve tratá-los como imutáveis quando os armazena no estado. Assim como com objetos, quando você quer atualizar um array armazenado no estado, você precisa criar um novo (ou fazer uma cópia de um existente), e então definir o estado para usar o novo array.

Você aprenderá

  • Como adicionar, remover ou alterar itens em um array no estado do React
  • Como atualizar um objeto dentro de um array
  • Como tornar a cópia de arrays menos repetitiva com Immer

Atualizando arrays sem mutação

Em JavaScript, arrays são apenas outro tipo de objeto. Assim como com objetos, você deve tratar arrays no estado do React como somente leitura. Isso significa que você não deve reatribuir itens dentro de um array como arr[0] = 'bird', e também não deve usar métodos que mutem o array, como push() e pop().

Em vez disso, toda vez que você quiser atualizar um array, você vai querer passar um novo array para sua função de definição de estado. Para fazer isso, você pode criar um novo array a partir do array original em seu estado chamando seus métodos não-mutantes como filter() e map(). Então você pode definir seu estado para o novo array resultante.

Aqui está uma tabela de referência de operações comuns de array. Ao lidar com arrays dentro do estado do React, você precisará evitar os métodos na coluna da esquerda, e em vez disso preferir os métodos na coluna da direita:

evitar (muta o array)preferir (retorna um novo array)
adicionarpush, unshiftconcat, [...arr] sintaxe spread (exemplo)
removerpop, shift, splicefilter, slice (exemplo)
substituirsplice, arr[i] = ... atribuiçãomap (exemplo)
ordenarreverse, sortcopie o array primeiro (exemplo)

Alternativamente, você pode usar Immer que permite usar métodos de ambas as colunas.

Pitfall

Infelizmente, slice e splice têm nomes similares mas são muito diferentes:

  • slice permite copiar um array ou uma parte dele.
  • splice muta o array (para inserir ou deletar itens).

No React, você estará usando slice (sem p!) muito mais frequentemente porque você não quer mutar objetos ou arrays no estado. Atualizando Objetos explica o que é mutação e por que não é recomendado para o estado.

Adicionando a um array

push() vai mutar um array, o que você não quer:

import { useState } from 'react';

let nextId = 0;

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState([]);

  return (
    <>
      <h1>Escultores inspiradores:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={() => {
        artists.push({
          id: nextId++,
          name: name,
        });
      }}>Adicionar</button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

Em vez disso, crie um novo array que contém os itens existentes e um novo item no final. Há múltiplas maneiras de fazer isso, mas a mais fácil é usar a sintaxe ... array spread:

setArtists( // Substitui o estado
[ // com um novo array
...artists, // que contém todos os itens antigos
{ id: nextId++, name: name } // e um novo item no final
]
);

Agora funciona corretamente:

import { useState } from 'react';

let nextId = 0;

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState([]);

  return (
    <>
      <h1>Escultores inspiradores:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={() => {
        setArtists([
          ...artists,
          { id: nextId++, name: name }
        ]);
      }}>Adicionar</button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

A sintaxe array spread também permite que você adicione um item no início colocando-o antes do ...artists original:

setArtists([
{ id: nextId++, name: name },
...artists // Coloca itens antigos no final
]);

Dessa forma, spread pode fazer o trabalho tanto de push() adicionando ao final de um array quanto de unshift() adicionando ao início de um array. Experimente no sandbox acima!

Removendo de um array

A maneira mais fácil de remover um item de um array é filtrá-lo. Em outras palavras, você vai produzir um novo array que não conterá esse item. Para fazer isso, use o método filter, por exemplo:

import { useState } from 'react';

let initialArtists = [
  { id: 0, name: 'Marta Colvin Andrade' },
  { id: 1, name: 'Lamidi Olonade Fakeye'},
  { id: 2, name: 'Louise Nevelson'},
];

export default function List() {
  const [artists, setArtists] = useState(
    initialArtists
  );

  return (
    <>
      <h1>Escultores inspiradores:</h1>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>
            {artist.name}{' '}
            <button onClick={() => {
              setArtists(
                artists.filter(a =>
                  a.id !== artist.id
                )
              );
            }}>
              Deletar
            </button>
          </li>
        ))}
      </ul>
    </>
  );
}

Clique no botão “Deletar” algumas vezes, e observe seu manipulador de clique.

setArtists(
artists.filter(a => a.id !== artist.id)
);

Aqui, artists.filter(a => a.id !== artist.id) significa “criar um array que consiste naqueles artists cujos IDs são diferentes de artist.id”. Em outras palavras, o botão “Deletar” de cada artista vai filtrar aquele artista do array, e então solicitar uma re-renderização com o array resultante. Note que filter não modifica o array original.

Transformando um array

Se você quiser alterar alguns ou todos os itens do array, você pode usar map() para criar um novo array. A função que você passará para map pode decidir o que fazer com cada item, baseado em seus dados ou seu índice (ou ambos).

Neste exemplo, um array armazena coordenadas de dois círculos e um quadrado. Quando você pressiona o botão, ele move apenas os círculos 50 pixels para baixo. Ele faz isso produzindo um novo array de dados usando map():

import { useState } from 'react';

let initialShapes = [
  { id: 0, type: 'circle', x: 50, y: 100 },
  { id: 1, type: 'square', x: 150, y: 100 },
  { id: 2, type: 'circle', x: 250, y: 100 },
];

export default function ShapeEditor() {
  const [shapes, setShapes] = useState(
    initialShapes
  );

  function handleClick() {
    const nextShapes = shapes.map(shape => {
      if (shape.type === 'square') {
        // Sem mudança
        return shape;
      } else {
        // Retorna um novo círculo 50px abaixo
        return {
          ...shape,
          y: shape.y + 50,
        };
      }
    });
    // Re-renderiza com o novo array
    setShapes(nextShapes);
  }

  return (
    <>
      <button onClick={handleClick}>
        Mover círculos para baixo!
      </button>
      {shapes.map(shape => (
        <div
          key={shape.id}
          style={{
          background: 'purple',
          position: 'absolute',
          left: shape.x,
          top: shape.y,
          borderRadius:
            shape.type === 'circle'
              ? '50%' : '',
          width: 20,
          height: 20,
        }} />
      ))}
    </>
  );
}

Substituindo itens em um array

É particularmente comum querer substituir um ou mais itens em um array. Atribuições como arr[0] = 'bird' estão mutando o array original, então em vez disso você vai querer usar map para isso também.

Para substituir um item, crie um novo array com map. Dentro de sua chamada map, você receberá o índice do item como segundo argumento. Use-o para decidir se deve retornar o item original (o primeiro argumento) ou algo diferente:

import { useState } from 'react';

let initialCounters = [
  0, 0, 0
];

export default function CounterList() {
  const [counters, setCounters] = useState(
    initialCounters
  );

  function handleIncrementClick(index) {
    const nextCounters = counters.map((c, i) => {
      if (i === index) {
        // Incrementa o contador clicado
        return c + 1;
      } else {
        // O resto não mudou
        return c;
      }
    });
    setCounters(nextCounters);
  }

  return (
    <ul>
      {counters.map((counter, i) => (
        <li key={i}>
          {counter}
          <button onClick={() => {
            handleIncrementClick(i);
          }}>+1</button>
        </li>
      ))}
    </ul>
  );
}

Inserindo em um array

Às vezes, você pode querer inserir um item em uma posição particular que não seja nem no início nem no final. Para fazer isso, você pode usar a sintaxe ... array spread junto com o método slice(). O método slice() permite cortar uma “fatia” do array. Para inserir um item, você criará um array que espalha a fatia antes do ponto de inserção, então o novo item, e então o resto do array original.

Neste exemplo, o botão Inserir sempre insere no índice 1:

import { useState } from 'react';

let nextId = 3;
const initialArtists = [
  { id: 0, name: 'Marta Colvin Andrade' },
  { id: 1, name: 'Lamidi Olonade Fakeye'},
  { id: 2, name: 'Louise Nevelson'},
];

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState(
    initialArtists
  );

  function handleClick() {
    const insertAt = 1; // Pode ser qualquer índice
    const nextArtists = [
      // Itens antes do ponto de inserção:
      ...artists.slice(0, insertAt),
      // Novo item:
      { id: nextId++, name: name },
      // Itens depois do ponto de inserção:
      ...artists.slice(insertAt)
    ];
    setArtists(nextArtists);
    setName('');
  }

  return (
    <>
      <h1>Escultores inspiradores:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={handleClick}>
        Inserir
      </button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

Fazendo outras mudanças em um array

Há algumas coisas que você não pode fazer apenas com a sintaxe spread e métodos não-mutantes como map() e filter(). Por exemplo, você pode querer reverter ou ordenar um array. Os métodos JavaScript reverse() e sort() estão mutando o array original, então você não pode usá-los diretamente.

No entanto, você pode copiar o array primeiro, e então fazer mudanças nele.

Por exemplo:

import { useState } from 'react';

const initialList = [
  { id: 0, title: 'Big Bellies' },
  { id: 1, title: 'Lunar Landscape' },
  { id: 2, title: 'Terracotta Army' },
];

export default function List() {
  const [list, setList] = useState(initialList);

  function handleClick() {
    const nextList = [...list];
    nextList.reverse();
    setList(nextList);
  }

  return (
    <>
      <button onClick={handleClick}>
        Reverter
      </button>
      <ul>
        {list.map(artwork => (
          <li key={artwork.id}>{artwork.title}</li>
        ))}
      </ul>
    </>
  );
}

Aqui, você usa a sintaxe spread [...list] para criar uma cópia do array original primeiro. Agora que você tem uma cópia, você pode usar métodos mutantes como nextList.reverse() ou nextList.sort(), ou até mesmo atribuir itens individuais com nextList[0] = "algo".

No entanto, mesmo se você copiar um array, você não pode mutar itens existentes dentro dele diretamente. Isso é porque a cópia é superficial—o novo array conterá os mesmos itens que o original. Então se você modificar um objeto dentro do array copiado, você está mutando o estado existente. Por exemplo, código como este é um problema.

const nextList = [...list];
nextList[0].seen = true; // Problema: muta list[0]
setList(nextList);

Embora nextList e list sejam dois arrays diferentes, nextList[0] e list[0] apontam para o mesmo objeto. Então ao mudar nextList[0].seen, você também está mudando list[0].seen. Esta é uma mutação de estado, que você deve evitar! Você pode resolver este problema de forma similar a atualizar objetos JavaScript aninhados—copiando itens individuais que você quer mudar em vez de mutá-los. Veja como.

Atualizando objetos dentro de arrays

Objetos não estão realmente localizados “dentro” de arrays. Eles podem parecer estar “dentro” no código, mas cada objeto em um array é um valor separado, para o qual o array “aponta”. É por isso que você precisa ter cuidado ao mudar campos aninhados como list[0]. A lista de obras de arte de outra pessoa pode apontar para o mesmo elemento do array!

Ao atualizar estado aninhado, você precisa criar cópias do ponto onde você quer atualizar, e todo o caminho até o nível superior. Vamos ver como isso funciona.

Neste exemplo, duas listas de obras de arte separadas têm o mesmo estado inicial. Elas deveriam estar isoladas, mas por causa de uma mutação, seu estado é acidentalmente compartilhado, e marcar uma caixa em uma lista afeta a outra lista:

import { useState } from 'react';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [myList, setMyList] = useState(initialList);
  const [yourList, setYourList] = useState(
    initialList
  );

  function handleToggleMyList(artworkId, nextSeen) {
    const myNextList = [...myList];
    const artwork = myNextList.find(
      a => a.id === artworkId
    );
    artwork.seen = nextSeen;
    setMyList(myNextList);
  }

  function handleToggleYourList(artworkId, nextSeen) {
    const yourNextList = [...yourList];
    const artwork = yourNextList.find(
      a => a.id === artworkId
    );
    artwork.seen = nextSeen;
    setYourList(yourNextList);
  }

  return (
    <>
      <h1>Lista de Arte</h1>
      <h2>Minha lista de arte para ver:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Sua lista de arte para ver:</h2>
      <ItemList
        artworks={yourList}
        onToggle={handleToggleYourList} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

O problema está no código como este:

const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // Problema: muta um item existente
setMyList(myNextList);

Embora o array myNextList em si seja novo, os itens em si são os mesmos que no array myList original. Então mudar artwork.seen muda o item de obra de arte original. Esse item de obra de arte também está em yourList, o que causa o bug. Bugs como este podem ser difíceis de pensar, mas felizmente eles desaparecem se você evitar mutar o estado.

Você pode usar map para substituir um item antigo por sua versão atualizada sem mutação.

setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Criar um *novo* objeto com mudanças
return { ...artwork, seen: nextSeen };
} else {
// Sem mudanças
return artwork;
}
}));

Aqui, ... é a sintaxe object spread usada para criar uma cópia de um objeto.

Com esta abordagem, nenhum dos itens de estado existentes está sendo mutado, e o bug é corrigido:

import { useState } from 'react';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [myList, setMyList] = useState(initialList);
  const [yourList, setYourList] = useState(
    initialList
  );

  function handleToggleMyList(artworkId, nextSeen) {
    setMyList(myList.map(artwork => {
      if (artwork.id === artworkId) {
        // Criar um *novo* objeto com mudanças
        return { ...artwork, seen: nextSeen };
      } else {
        // Sem mudanças
        return artwork;
      }
    }));
  }

  function handleToggleYourList(artworkId, nextSeen) {
    setYourList(yourList.map(artwork => {
      if (artwork.id === artworkId) {
        // Criar um *novo* objeto com mudanças
        return { ...artwork, seen: nextSeen };
      } else {
        // Sem mudanças
        return artwork;
      }
    }));
  }

  return (
    <>
      <h1>Lista de Arte</h1>
      <h2>Minha lista de arte para ver:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Sua lista de arte para ver:</h2>
      <ItemList
        artworks={yourList}
        onToggle={handleToggleYourList} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

Em geral, você deve apenas mutar objetos que você acabou de criar. Se você estivesse inserindo uma nova obra de arte, você poderia mutá-la, mas se você está lidando com algo que já está no estado, você precisa fazer uma cópia.

Escreva lógica de atualização concisa com Immer

Atualizar arrays aninhados sem mutação pode ficar um pouco repetitivo. Assim como com objetos:

  • Geralmente, você não deveria precisar atualizar o estado mais do que alguns níveis de profundidade. Se seus objetos de estado são muito profundos, você pode querer reestruturá-los de forma diferente para que sejam planos.
  • Se você não quer mudar sua estrutura de estado, você pode preferir usar Immer, que permite escrever usando a sintaxe conveniente mas mutante e cuida de produzir as cópias para você.

Aqui está o exemplo da Lista de Arte reescrito com Immer:

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

Note como com Immer, mutação como artwork.seen = nextSeen agora está ok:

updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});

Isso é porque você não está mutando o estado original, mas você está mutando um objeto draft especial fornecido pelo Immer. Similarmente, você pode aplicar métodos mutantes como push() e pop() ao conteúdo do draft.

Por trás dos panos, Immer sempre constrói o próximo estado do zero de acordo com as mudanças que você fez no draft. Isso mantém seus manipuladores de evento muito concisos sem nunca mutar o estado.

Recap

  • Você pode colocar arrays no estado, mas você não pode mudá-los.
  • Em vez de mutar um array, crie uma nova versão dele, e atualize o estado para ela.
  • Você pode usar a sintaxe [...arr, newItem] array spread para criar arrays com novos itens.
  • Você pode usar filter() e map() para criar novos arrays com itens filtrados ou transformados.
  • Você pode usar Immer para manter seu código conciso.

Challenge 1 of 4:
Atualizar um item no carrinho de compras

Preencha a lógica de handleIncreaseClick para que pressionar ”+” aumente o número correspondente:

import { useState } from 'react';

const initialProducts = [{
  id: 0,
  name: 'Baklava',
  count: 1,
}, {
  id: 1,
  name: 'Queijo',
  count: 5,
}, {
  id: 2,
  name: 'Espaguete',
  count: 2,
}];

export default function ShoppingCart() {
  const [
    products,
    setProducts
  ] = useState(initialProducts)

  function handleIncreaseClick(productId) {

  }

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          {product.name}
          {' '}
          (<b>{product.count}</b>)
          <button onClick={() => {
            handleIncreaseClick(product.id);
          }}>
            +
          </button>
        </li>
      ))}
    </ul>
  );
}