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

Renderizar listas no React com map e a prop key

Como transformar um array em elementos com map, o que a key faz por dentro e por que usar o índice quebra a lista assim que um item é removido.

Rodolfo Mori12 min de leitura

Você remove a primeira pessoa de uma lista e a tela continua com a quantidade certa de linhas. Mesmo assim, a observação digitada para Ana aparece ao lado de Bruno. O erro parece estar nos dados, mas pode estar na key usada pelo React.

Nesta aula, você vai transformar uma lista JavaScript em elementos React, reproduzir esse bug e corrigi-lo. Começaremos com três nomes e chegaremos a uma tela de chamada com campo de observação, reordenação e estado vazio.

O porquê vem antes do map: quando uma lista muda, React precisa reconhecer o que permaneceu, o que entrou e o que saiu. Um array é uma lista de valores; map cria um novo array transformando cada item; JSX é a sintaxe parecida com HTML usada para descrever a interface. Renderizar é transformar essa descrição no que aparece na tela.

A key é o identificador estável de cada elemento entre uma renderização e outra. Os exemplos usam uma turma fictícia; o campo de observação deixa visível quando a identidade errada preserva o estado, a memória temporária da tela, na linha errada.

Por que a key precisa ser estável?

Porque React usa a key para reconhecer o mesmo item mesmo quando sua posição muda. O identificador deve acompanhar o dado, não o lugar ocupado na lista.

Imagine mochilas numa fila: cada mochila recebe a etiqueta do aluno. Se a fila muda, a etiqueta ainda liga mochila e dono. Usar o índice como key seria trocar a etiqueta por “primeira mochila” ou “segunda mochila”; ao remover alguém, o conteúdo pode acompanhar a posição errada. Na comparação, aluno é o item, etiqueta é a key e lugar na fila é o índice. O limite é que React não move objetos físicos: ele compara descrições de elementos e decide quais nós da tela reaproveitar.

O exemplo mínimo usa um array de objetos. Cada objeto agrupa id, nome e nota; o map devolve um pedaço de JSX para cada aluno.

jsx
const alunos = [
  { id: 'a-2201', nome: 'Ana Prado', nota: 8.5 },
  { id: 'a-2202', nome: 'Bruno Lima', nota: 6.0 },
  { id: 'a-2203', nome: 'Carla Souza', nota: 9.2 },
];

function ListaDeChamada() {
  return (
    <ul>
      {alunos.map((aluno) => (
        <li key={aluno.id}>
          {aluno.nome}{aluno.nota}
        </li>
      ))}
    </ul>
  );
}
<ul><li>Ana Prado — 8.5</li><li>Bruno Lima — 6</li><li>Carla Souza — 9.2</li></ul>

alunos.map visita cada objeto e chama a função (aluno) => (...), chamada de callback porque é entregue ao map para ele executá-la. Ela cria um <li>; as chaves do JSX inserem nome e nota, e key={aluno.id} liga a linha ao aluno.

Esse HTML veio de renderToStaticMarkup, uma função de react-dom/server que converte elementos React em texto HTML sem abrir o navegador. Toda saída HTML desta lição foi gerada assim.

Repare que 6.0 saiu como 6: em JavaScript, 6.0 e 6 são o mesmo número, e a casa decimal só existe quando você a formata. Para boletim, nota.toFixed(1).

Experimente você mesmo: onde a key fica guardada?

Ela fica no elemento React, fora de props, o objeto com os dados entregues ao elemento. Antes de ler a saída, preveja se linhas será um array, quantos itens terá e onde aparecerá a-2201; depois compare com o primeiro item impresso.

jsx
const linhas = alunos.map((aluno) => <li key={aluno.id}>{aluno.nome}</li>);

console.log(Array.isArray(linhas), linhas.length);
console.log(linhas[0]);
true 3 { '$$typeof': Symbol(react.transitional.element), type: 'li', key: 'a-2201', props: { children: 'Ana Prado' }, _owner: null, _store: {} }

O resultado confirma: map devolveu três objetos que descrevem a tela, não HTML nem texto. type informa a tag, props reúne os dados entregues ao elemento e key fica do lado de fora porque pertence ao controle interno do React.

(aluno) => (...) é uma arrow function, uma escrita curta para função. Com parênteses, ela retorna o JSX automaticamente; com chaves, o retorno precisa ser explícito.

jsx
function ListaComChaves() {
  return (
    <ul>
      {alunos.map((aluno) => {
        <li key={aluno.id}>{aluno.nome}</li>;
      })}
    </ul>
  );
}
<ul></ul>

Uma <ul> vazia, sem aviso, sem tela vermelha. É o primeiro lugar para olhar quando a lista “não aparece”.

O que o React faz com a key durante a reconciliação?

Quando o estado muda, React renderiza de novo e compara a árvore nova com a anterior. Essa comparação é a reconciliação; entre os filhos de uma lista, a key indica quais elementos representam o mesmo item.

árvore antiga árvore nova a-2201 · Ana a-2202 · Bruno a-2203 · Carla a-2202 · Bruno a-2203 · Carla removida mesma key mesma key

Key igual, com o mesmo tipo de elemento, significa “é o mesmo item”: React pode manter o nó do DOM, a árvore de elementos da página, e atualizar seu conteúdo. Se a key ou o tipo muda, o nó antigo pode sair e um novo entrar.

Sem key, o React não tem esse critério e cai na posição — e reclama:

jsx
function ListaSemKey() {
  return (
    <ul>
      {alunos.map((aluno) => (
        <li>{aluno.nome}</li>
      ))}
    </ul>
  );
}
Each child in a list should have a unique "key" prop.

Check the render method of ListaSemKey. See https://react.dev/link/warning-keys for more information.

A lista ainda aparece na tela: esse é um aviso, não uma exceção. Por isso muita gente convive com ele por meses. O detalhe do diagnóstico está no artigo sobre o erro Each child in a list should have a unique key prop.

Até aqui: map transforma cada dado em elemento React; a key dá identidade a esse elemento; e a reconciliação usa essa identidade para preservar o nó certo.

Por que o índice do array é uma key ruim?

Porque o índice identifica a posição, não o item. Ao remover, inserir ou ordenar uma linha, a mesma posição passa a representar outro dado.

A correção mais rápida que o aviso provoca é esta:

jsx
{alunos.map((aluno, indice) => (
  <li key={indice}>{aluno.nome}</li>
))}

O aviso some. O problema não. Quando alguém sai, a posição 1 continua existindo, mas agora contém outro aluno. React a considera o mesmo item e preserva o que estava associado àquele nó.

“Tudo” inclui o texto digitado num input, a posição da rolagem (scroll), o foco, o estado interno de um componente filho e qualquer animação em andamento.

O que acontece quando a key usa o índice?

O estado local pode acompanhar a posição e aparecer no item errado. No exemplo, remover Ana faz a observação dela aparecer ao lado de Bruno.

Para ver isso acontecer sem depender de um navegador aberto, montei a mesma lista duas vezes — uma com key={indice}, outra com key={aluno.id} — dentro de um DOM simulado pelo jsdom. act agrupa e conclui as atualizações do React durante o teste. O campo é um input não controlado: o texto digitado fica no próprio nó do DOM, em vez de ser guardado pelo estado do componente.

Experimente você mesmo: de quem fica cada observação?

Com key={indice}, a observação acompanha a posição; com key={aluno.id}, acompanha o aluno. Antes de ler a saída, preveja as duas linhas restantes depois de remover Ana e anote qual texto deve aparecer ao lado de Bruno e Carla.

jsx
const turma = [
  { id: 'a-2201', nome: 'Ana Prado' },
  { id: 'a-2202', nome: 'Bruno Lima' },
  { id: 'a-2203', nome: 'Carla Souza' },
];

function Chamada({ alunos, chave }) {
  return (
    <ul>
      {alunos.map((aluno, indice) => (
        <li key={chave === 'indice' ? indice : aluno.id}>
          <span>{aluno.nome}</span>
          <input defaultValue="" />
        </li>
      ))}
    </ul>
  );
}

O roteiro do experimento: renderiza a turma inteira, a professora digita uma observação em cada linha, e então Ana Prado tranca a matrícula e sai do array. O trecho abaixo roda duas vezes, uma com chave valendo 'indice' e outra valendo 'id' — o rotulo é só para separar as duas rodadas na saída.

jsx
// lê o nome e o que está digitado em cada linha que existe no DOM
function fotografar(raiz) {
  return [...raiz.querySelectorAll('li')]
    .map((li) => `${li.querySelector('span').textContent}: "${li.querySelector('input').value}"`)
    .join('\n');
}

// document vem do jsdom; act sai do próprio 'react' no 19, e
// createRoot, de 'react-dom/client'
const container = document.createElement('div');
document.body.appendChild(container);
const root = createRoot(container);

const rotulo = chave === 'indice' ? 'key={indice}' : 'key={aluno.id}';

await act(async () => root.render(<Chamada alunos={turma} chave={chave} />));

const inputs = [...container.querySelectorAll('input')];
inputs[0].value = 'faltou na prova';
inputs[1].value = 'entregou o trabalho';
inputs[2].value = 'pediu segunda chamada';

console.log(`--- ${rotulo} — antes`);
console.log(fotografar(container));

// Ana trancou a matrícula: a primeira linha sai
await act(async () => root.render(<Chamada alunos={turma.slice(1)} chave={chave} />));

console.log(`--- ${rotulo} — depois de remover Ana Prado`);
console.log(fotografar(container));
--- key={indice} — antes Ana Prado: "faltou na prova" Bruno Lima: "entregou o trabalho" Carla Souza: "pediu segunda chamada" --- key={indice} — depois de remover Ana Prado Bruno Lima: "faltou na prova" Carla Souza: "entregou o trabalho"

— key={aluno.id} — antes Ana Prado: “faltou na prova” Bruno Lima: “entregou o trabalho” Carla Souza: “pediu segunda chamada” — key={aluno.id} — depois de remover Ana Prado Bruno Lima: “entregou o trabalho” Carla Souza: “pediu segunda chamada”

Leia o primeiro “depois” com calma. Com key={indice}, Bruno Lima ficou com “faltou na prova” — a observação da Ana. Carla herdou a do Bruno. E a observação da Carla, que era a última, desapareceu junto com o último li.

Ninguém digitou errado. O React fez exatamente o que foi instruído: manter o nó da posição 0 e da posição 1, e jogar fora o da posição 2. Com key={aluno.id}, a mesma remoção manda embora o nó certo e cada observação continua colada no seu aluno.

Esse é o tipo de bug que passa por revisão de código (code review), por um teste manual rápido e aparece na segunda-feira como “o sistema trocou a nota dos alunos”.

Por que Math.random não deve ser usado como key?

Porque ele cria uma identidade nova em toda renderização. React entende que todos os itens antigos saíram, monta outros nós e perde o estado local.

Vendo o aviso sumir com qualquer valor único, aparece a ideia de gerar a key na hora:

jsx
function Chamada({ alunos }) {
  return (
    <ul>
      {alunos.map((aluno) => (
        <li key={Math.random()}>
          <span>{aluno.nome}</span>
          <input defaultValue="" />
        </li>
      ))}
    </ul>
  );
}

O teste é o mesmo de antes, com um detalhe: a lista não muda entre as duas renderizações.

jsx
await act(async () => root.render(<Chamada alunos={turma} />));

const primeiroInput = container.querySelector('input');
primeiroInput.value = 'faltou na prova';
console.log('digitado:', primeiroInput.value);

await act(async () => root.render(<Chamada alunos={turma} />));

console.log('depois do re-render:', JSON.stringify(container.querySelector('input').value));
console.log('o input continua sendo o mesmo nó?', container.querySelector('input') === primeiroInput);
digitado: faltou na prova depois do re-render: "" o input continua sendo o mesmo nó? false

A lista nem mudou: houve apenas um re-render, uma nova execução do componente para atualizar a tela. O artigo sobre re-render no React detalha seus gatilhos. Como toda key nasceu diferente, React trocou todas as linhas e o texto digitado desapareceu.

crypto.randomUUID(), que gera um identificador aleatório, faz a mesma coisa quando chamado dentro do map: no mesmo teste, o resultado foi "" e false. Já Date.now() falha de outro jeito, e vale conhecer a diferença: as três linhas do mesmo map são criadas no mesmo milissegundo, então recebem todas a mesma key. Em vez de destruir tudo, o React cai no problema da próxima seção.

O que acontece quando duas keys são iguais?

React perde a capacidade de distinguir os irmãos com a mesma identidade. O resultado não é suportado: itens podem ser omitidos ou duplicados.

Sem id à mão, o segundo palpite costuma ser o nome. Funciona até a turma ter duas alunas homônimas:

jsx
const homonimas = [
  { id: 'a-2201', nome: 'Ana Prado' },
  { id: 'a-2204', nome: 'Ana Prado' },
  { id: 'a-2202', nome: 'Bruno Lima' },
];

function ChamadaPorNome({ lista }) {
  return (
    <ul>
      {lista.map((aluno) => (
        <li key={aluno.nome}>{aluno.nome}</li>
      ))}
    </ul>
  );
}

await act(async () => root.render(<ChamadaPorNome lista={homonimas} />));
console.log('linhas na tela:', container.querySelectorAll('li').length);

await act(async () => root.render(<ChamadaPorNome lista={[...homonimas].reverse()} />));
console.log('depois de reordenar:', container.querySelectorAll('li').length);
Encountered two children with the same key, `Ana Prado`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

Esse aviso volta a cada renderização — não é um alerta que aparece só na primeira e some. E a contagem de linhas mostra o que ele está antecipando:

linhas na tela: 3 depois de reordenar: 4

Quatro linhas para três alunas. O aviso diz “may cause children to be duplicated and/or omitted”, e a contagem mostra a duplicação acontecendo. A tela passa a mentir sobre o próprio banco de dados.

Qual valor usar como key no React?

A key precisa ser única entre os irmãos e a mesma para o mesmo item em toda renderização. Prefira o id salvo no banco; se nenhum campo for único sozinho, use uma combinação estável. Na prática, siga esta ordem:

origem da key quando usar por que funciona
aluno.id do banco sempre que o dado vier de uma API é único e não muda
chave composta, como ${aula}:${alunoId} linha que só existe pela combinação de dois campos a combinação é única
crypto.randomUUID() gerado ao criar o item item novo que ainda não foi salvo nasce uma vez e vai para o estado junto com o objeto
índice do array lista fixa, que nunca reordena nem encolhe a posição nunca deixa de ser o item
Math.random() ou crypto.randomUUID() na renderização nunca muda a cada render e destrói o nó
Date.now() na renderização nunca o map inteiro cai no mesmo milissegundo e duplica a key

O caso da chave composta aparece toda hora em relatório. A tabela de frequência não tem id próprio: cada linha é o encontro de uma aula com um aluno.

jsx
const presencas = [
  { aula: '2026-05-11', alunoId: 'a-2201', nome: 'Ana Prado', presente: true },
  { aula: '2026-05-11', alunoId: 'a-2202', nome: 'Bruno Lima', presente: false },
  { aula: '2026-05-13', alunoId: 'a-2201', nome: 'Ana Prado', presente: true },
  { aula: '2026-05-13', alunoId: 'a-2202', nome: 'Bruno Lima', presente: true },
];

// dentro de <ul>
{presencas.map((p) => (
  <li key={`${p.aula}:${p.alunoId}`}>
    {p.aula}{p.nome}{p.presente ? 'presente' : 'faltou'}
  </li>
))}
<ul><li>2026-05-11 — Ana Prado — presente</li><li>2026-05-11 — Bruno Lima — faltou</li><li>2026-05-13 — Ana Prado — presente</li><li>2026-05-13 — Bruno Lima — presente</li></ul>

O aluno.id sozinho se repetiria em duas linhas; a data sozinha também. Juntos, identificam a linha. O operador ternário, um if curto dentro de uma expressão, faz a renderização condicional, o mesmo assunto da lição anterior desta trilha.

É possível ler a key dentro do componente filho?

Não. React consome a key antes de montar props, o objeto com os valores entregues ao componente. Se o filho precisa do id, passe-o separadamente.

jsx
function LinhaDoAluno(props) {
  console.log('props recebidas:', props);
  return <li>{props.nome}</li>;
}

function Chamada() {
  return (
    <ul>
      {alunos.map((aluno) => (
        <LinhaDoAluno key={aluno.id} nome={aluno.nome} />
      ))}
    </ul>
  );
}
props recebidas: { nome: 'Ana Prado' } props recebidas: { nome: 'Bruno Lima' } props recebidas: { nome: 'Carla Souza' }

nome chegou, confirmando que key fica fora de props. Se o filho precisa do identificador, passe-o também como id={aluno.id}. Vale o mesmo para o JSX que você escreve em qualquer componente.

Como usar key em listas aninhadas?

Dê uma key aos elementos criados por cada map. Numa tela de boletim, a turma é identificada entre turmas e o aluno entre os alunos da mesma turma; a unicidade vale entre irmãos, não no aplicativo inteiro.

jsx
const turmas = [
  {
    id: 't-9a',
    nome: '9º A',
    alunos: [
      { id: 'a-2201', nome: 'Ana Prado' },
      { id: 'a-2202', nome: 'Bruno Lima' },
    ],
  },
  { id: 't-9b', nome: '9º B', alunos: [{ id: 'a-2301', nome: 'Diego Alves' }] },
];

// dentro de <div>
{turmas.map((turma) => (
  <section key={turma.id}>
    <h2>{turma.nome}</h2>
    <ul>
      {turma.alunos.map((aluno) => (
        <li key={aluno.id}>{aluno.nome}</li>
      ))}
    </ul>
  </section>
))}
<div><section><h2>9º A</h2><ul><li>Ana Prado</li><li>Bruno Lima</li></ul></section><section><h2>9º B</h2><ul><li>Diego Alves</li></ul></section></div>

Quando cada item gera duas tags irmãs sem um elemento visual em volta, use Fragment, o agrupador invisível do React. A key exige a forma escrita por extenso, porque a abreviação <> não aceita atributos:

jsx
import { Fragment } from 'react';

// dentro de <dl>
{alunos.map((aluno) => (
  <Fragment key={aluno.id}>
    <dt>{aluno.nome}</dt>
    <dd>{aluno.nota}</dd>
  </Fragment>
))}
<dl><dt>Ana Prado</dt><dd>8.5</dd><dt>Bruno Lima</dt><dd>6</dd><dt>Carla Souza</dt><dd>9.2</dd></dl>

Como renderizar uma lista vazia sem mostrar zero?

Compare o tamanho explicitamente ou retorne uma mensagem quando ele for zero. Usar apenas alunos.length && ... pode renderizar o próprio número 0.

Toda lista real fica vazia em algum momento — cadastro novo, filtro sem resultado ou API, o serviço que fornece os dados, que devolveu uma lista vazia. O operador lógico && parece resolver, mas tem uma armadilha:

jsx
function Chamada({ alunos }) {
  return (
    <div>
      {alunos.length && (
        <ul>
          {alunos.map((aluno) => (
            <li key={aluno.id}>{aluno.nome}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

console.log(renderToStaticMarkup(<Chamada alunos={[{ id: 'a-2201', nome: 'Ana Prado' }]} />));
console.log(renderToStaticMarkup(<Chamada alunos={[]} />));
<div><ul><li>Ana Prado</li></ul></div> <div>0</div>

A segunda linha deveria ser uma div vazia. Com a lista vazia, alunos.length é 0; o && devolve esse 0, e 0 não é como false: o React renderiza zero na tela. Aparece um 0 solto no meio do layout e ninguém entende de onde veio.

Por isso, trate o vazio como uma tela e compare o tamanho explicitamente:

jsx
function Chamada({ alunos, busca }) {
  const filtrados = alunos.filter((a) =>
    a.nome.toLowerCase().includes(busca.toLowerCase()),
  );

  if (filtrados.length === 0) {
    return <p>Nenhum aluno encontrado nesta turma.</p>;
  }

  return (
    <ul>
      {filtrados.map((aluno) => (
        <li key={aluno.id}>{aluno.nome}</li>
      ))}
    </ul>
  );
}

// a turma tem Ana Prado, Bruno Lima e Carla Souza
console.log(renderToStaticMarkup(<Chamada alunos={turma} busca="a" />));
console.log(renderToStaticMarkup(<Chamada alunos={turma} busca="zz" />));
<ul><li>Ana Prado</li><li>Bruno Lima</li><li>Carla Souza</li></ul> <p>Nenhum aluno encontrado nesta turma.</p>

filter cria um array apenas com itens que passam pela condição; depois map transforma os sobreviventes em elementos. Filtrar e ordenar derivam dos dados atuais e não precisam virar outro estado.

O que aprender depois de listas e key?

O próximo passo é reagir ao clique de adicionar ou remover uma linha. A lição de eventos no React mostra como conectar essas ações à interface.

Até aqui: você transformou dados com map, escolheu keys estáveis, reproduziu os bugs de índice, aleatoriedade e duplicação, tratou listas aninhadas e exibiu uma mensagem para o estado vazio.

Para adicionar e remover alunos de verdade, o array também precisa morar no estado; veja useState no React. O caminho completo, com a ordem de estudo, está na trilha de React.

  • react
  • listas
  • map
  • key
  • arrays

Perguntas frequentes

Posso usar o índice como key alguma vez?
Pode, quando a lista é estática e nunca muda de ordem nem de tamanho enquanto está na tela — um menu fixo, por exemplo. No instante em que existe adicionar, remover, ordenar ou filtrar, o índice deixa de identificar o item e vira só a posição da linha.
Como leio a key dentro do componente filho?
Não lê. A key é consumida pelo React e não chega em props. Se o filho precisa do identificador, passe de novo com outro nome, como id={aluno.id} ao lado de key={aluno.id}.
A key precisa ser única no aplicativo inteiro?
Não. Ela só precisa ser única entre os irmãos daquela mesma lista. Duas listas diferentes podem repetir a mesma key sem nenhum problema.
O que fazer quando o item ainda não tem id porque nem foi salvo?
Gere o identificador no momento em que o item entra no estado, com crypto.randomUUID(), e guarde junto com o objeto. O que não funciona é gerar durante a renderização, porque aí ele muda a cada re-render.
A key precisa ser string, ou pode ser número?
Pode ser número, mas o React converte para string antes de guardar. Um elemento criado com key={2201} guarda a string "2201", então key={2201} e key={'2201'} são a mesma key. Isso importa quando a lista mistura id numérico e id de texto: 2201 e "2201" colidem em vez de se separarem.

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, React 19.2.8, react-dom/client em jsdom 30.0.1, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. React — Rendering Lists — react.dev
  2. React — Preserving and Resetting State — react.dev
  3. MDN — Array.prototype.map() — developer.mozilla.org

Continue por aqui