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.
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.
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>
);
}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.
const linhas = alunos.map((aluno) => <li key={aluno.id}>{aluno.nome}</li>);
console.log(Array.isArray(linhas), linhas.length);
console.log(linhas[0]);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.
function ListaComChaves() {
return (
<ul>
{alunos.map((aluno) => {
<li key={aluno.id}>{aluno.nome}</li>;
})}
</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.
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:
function ListaSemKey() {
return (
<ul>
{alunos.map((aluno) => (
<li>{aluno.nome}</li>
))}
</ul>
);
}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:
{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.
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.
// 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={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:
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.
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);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:
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);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:
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.
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>
))}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.
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>
);
}Só 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.
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>
))}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:
import { Fragment } from 'react';
// dentro de <dl>
{alunos.map((aluno) => (
<Fragment key={aluno.id}>
<dt>{aluno.nome}</dt>
<dd>{aluno.nota}</dd>
</Fragment>
))}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:
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={[]} />));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:
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" />));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.
Perguntas frequentes
Posso usar o índice como key alguma vez?
Como leio a key dentro do componente filho?
A key precisa ser única no aplicativo inteiro?
O que fazer quando o item ainda não tem id porque nem foi salvo?
A key precisa ser string, ou pode ser número?
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, 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
- React — Rendering Lists — react.dev
- React — Preserving and Resetting State — react.dev
- MDN — Array.prototype.map() — developer.mozilla.org


