Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
Erro resolvidoIniciantecódigo testado

Each child in a list should have a unique key prop

O aviso de key no console do React: por que ele aparece, o que quebra de verdade quando a key repete ou usa índice, e como escolher a chave certa.

Rodolfo Mori13 min de leitura

Each child in a list should have a unique "key" prop. é o aviso de que os elementos irmãos produzidos por uma lista não receberam identidades estáveis. Na primeira renderização tudo pode parecer correto; o problema aparece quando o React precisa reconciliar itens que mudaram de posição, entraram ou saíram.

Pense no guarda-volumes de um evento. A key é o número permanente do ticket; o índice é apenas a posição atual na fila. Se alguém sai do meio, as posições mudam, mas cada mala continua pertencendo ao mesmo ticket. No React, a chave permite associar o elemento novo ao nó e ao estado anteriores. Uma key instável faz o React reaproveitar a linha errada ou desmontá-la e criar outra.

Todos os exemplos aqui são a lista de chamada do 9º ano A de uma escola: matrícula, nome, presença e nota. É o mesmo cenário de renderizar listas no React com map, só que aqui o foco é o que dá errado.

A mensagem completa e o componente que ela aponta

O aviso nasce de um .map() que devolve elementos sem key:

jsx
const alunos = [
  { matricula: '2026-014', nome: 'Ana Beatriz', turma: '9A' },
  { matricula: '2026-027', nome: 'Caio Ferraz', turma: '9A' },
  { matricula: '2026-031', nome: 'Diana Prado', turma: '9A' },
];

function ListaDeChamada() {
  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 ListaDeChamada. See https://react.dev/link/warning-keys for more information.

Repare na segunda linha: Check the render method of ListaDeChamada. Esse nome é o presente que a mensagem te dá. Num projeto com trinta componentes, ele diz exatamente qual arquivo abrir — e não é o componente da linha, é o componente que chamou o .map(). Quem produziu o array é quem tinha que carimbar cada item.

E a lista renderizou. O HTML saiu completo, com os três <li> na ordem certa:

jsx
console.log(document.getElementById('root').innerHTML);
<ul><li>Ana Beatriz</li><li>Caio Ferraz</li><li>Diana Prado</li></ul>

É por isso que o aviso é tão fácil de ignorar. A tela está certa. O que está errado é o que o React sabe sobre ela.

Onde o React procura a key — não é onde você imagina

A key vai no elemento que a função do map devolve. Não adianta pôr num filho dele. Este código continua avisando:

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

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

O <span> tem key, o <li> não. E é o <li> que é irmão dos outros dentro do <ul> — a key precisa estar no topo do que sai do map, seja uma tag HTML, seja um componente seu.

Quando o item vira um componente, aparece a segunda pegadinha: key não é uma prop. O React tira essa chave do objeto antes de montar as props:

jsx
function LinhaDoAluno(props) {
  console.log('props que chegaram:', Object.keys(props), '| props.key =', props.key);
  return <li>{props.nome}</li>;
}

function ListaCerta() {
  return (
    <ul>
      {alunos.map((aluno) => (
        <LinhaDoAluno key={aluno.matricula} nome={aluno.nome} />
      ))}
    </ul>
  );
}
LinhaDoAluno: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props) props que chegaram: [ 'nome' ] | props.key = undefined props que chegaram: [ 'nome' ] | props.key = undefined props que chegaram: [ 'nome' ] | props.key = undefined

Três alunos, três linhas de log, e em nenhuma delas props.key tem valor. O aviso sobre key is not a prop aparece uma vez só porque o React não repete a mesma mensagem para o mesmo componente.

Se o componente precisa da matrícula para alguma coisa, passe de novo com outro nome: <LinhaDoAluno key={aluno.matricula} matricula={aluno.matricula} />. Parece redundante, e não é: um valor é para o React, o outro é para você.

A terceira armadilha é o espalhamento. Se o objeto do aluno já tem um campo chamado key e você espalha ele no JSX, o React reclama antes mesmo de renderizar:

jsx
// a API já devolve o campo chamado "key"
const daApi = [
  { key: '2026-014', nome: 'Ana Beatriz' },
  { key: '2026-027', nome: 'Caio Ferraz' },
];

function ListaEspalhada() {
  return (
    <ul>
      {daApi.map((aluno) => (
        <li {...aluno}>{aluno.nome}</li>
      ))}
    </ul>
  );
}
A props object containing a "key" prop is being spread into JSX: let props = {key: someKey, nome: ..., children: ...}; <li {...props} /> React keys must be passed directly to JSX without using spread: let props = {nome: ..., children: ...}; <li key={someKey} {...props} />

E olhe o HTML que saiu: o campo nome virou atributo HTML no <li>, porque espalhar objeto de dado em tag nativa despeja tudo no DOM.

<ul><li nome="Ana Beatriz">Ana Beatriz</li><li nome="Caio Ferraz">Caio Ferraz</li></ul>

Key repetida: o aviso muda de texto e de gravidade

Existe um segundo aviso, e ele é bem mais sério que o primeiro. Aparece quando duas linhas irmãs recebem a mesma key — o caso clássico é a planilha da secretaria que importou o mesmo aluno duas vezes:

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

const turmaImportada = [
  { matricula: '2026-014', nome: 'Ana Beatriz' },
  { matricula: '2026-027', nome: 'Caio Ferraz' },
  { matricula: '2026-027', nome: 'Caio Ferraz (2)' },
  { matricula: '2026-031', nome: 'Diana Prado' },
];

// a professora clica em "ordenar por nota" e a lista muda de ordem
const porNota = [turmaImportada[1], turmaImportada[0], turmaImportada[2], turmaImportada[3]];

const linhas = () => [...document.querySelectorAll('li')].map((li) => li.textContent);
const root = createRoot(document.getElementById('root'));

await act(() => root.render(<ListaDeChamada alunos={turmaImportada} />));
console.log('array:', turmaImportada.length, 'alunos | tela:', linhas().length, 'linhas ->', linhas().join(' | '));

await act(() => root.render(<ListaDeChamada alunos={porNota} />));
console.log('array:', porNota.length, 'alunos | tela:', linhas().length, 'linhas ->', linhas().join(' | '));
Encountered two children with the same key, `2026-027`. 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. array: 4 alunos | tela: 4 linhas -> Ana Beatriz | Caio Ferraz | Caio Ferraz (2) | Diana Prado Encountered two children with the same key, `2026-027`. 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. array: 4 alunos | tela: 5 linhas -> Caio Ferraz | Caio Ferraz | Ana Beatriz | Caio Ferraz (2) | Diana Prado

Leia com calma a última linha. O array tem quatro alunos e a tela tem cinco linhas. O Caio apareceu duas vezes. Não é um aviso teórico sobre boas práticas: é a tela mostrando um aluno que não existe no dado. E repare que na primeira renderização a tela estava certa: o aviso já tinha saído — ele se repete a cada render, por isso aparece duas vezes aí em cima —, mas a linha fantasma só nasceu no update.

O bug de verdade: o input que troca de linha

Agora o caso que faz o time perder uma tarde. Cada linha da chamada tem um campo de nota. O campo guarda o que foi digitado — esse valor mora no DOM, não no seu array. É a key que diz ao React qual campo pertence a qual aluno.

jsx
function LinhaDeNota({ aluno }) {
  return (
    <li>
      <label>{aluno.nome}</label>
      <input type="text" defaultValue="" />
    </li>
  );
}

function Chamada({ alunos, usarIndice }) {
  return (
    <ul>
      {alunos.map((aluno, indice) =>
        usarIndice ? (
          <LinhaDeNota key={indice} aluno={aluno} />
        ) : (
          <LinhaDeNota key={aluno.matricula} aluno={aluno} />
        ),
      )}
    </ul>
  );
}

O roteiro do teste é o de uma professora real: ela digita as três notas e só depois descobre que a Ana trancou a matrícula e sai da lista.

jsx
const turma = [
  { matricula: '2026-014', nome: 'Ana Beatriz' },
  { matricula: '2026-027', nome: 'Caio Ferraz' },
  { matricula: '2026-031', nome: 'Diana Prado' },
];

const foto = () =>
  [...document.querySelectorAll('li')]
    .map((li) => `${li.querySelector('label').textContent}=${li.querySelector('input').value}`)
    .join('  ');

async function rodar(usarIndice) {
  const root = createRoot(document.getElementById('root'));
  await act(() => root.render(<Chamada alunos={turma} usarIndice={usarIndice} />));

  ['8.5', '7.0', '9.5'].forEach((nota, i) => {
    document.querySelectorAll('input')[i].value = nota;
  });
  console.log('digitado:', foto());

  await act(() => root.render(<Chamada alunos={turma.slice(1)} usarIndice={usarIndice} />));
  console.log('sem Ana :', foto());
  await act(() => root.unmount());
}

Primeiro com o índice como key:

jsx
await rodar(true);
digitado: Ana Beatriz=8.5 Caio Ferraz=7.0 Diana Prado=9.5 sem Ana : Caio Ferraz=8.5 Diana Prado=7.0

Agora exatamente o mesmo roteiro, mudando só a key:

jsx
await rodar(false);
digitado: Ana Beatriz=8.5 Caio Ferraz=7.0 Diana Prado=9.5 sem Ana : Caio Ferraz=7.0 Diana Prado=9.5

Com key={indice}, o Caio ficou com o 8,5 da Ana. Ninguém digitou isso. O React viu que a posição 0 continuava existindo, achou que era o mesmo item, manteve o mesmo <input> no lugar e só trocou o texto do <label>. A nota ficou onde estava enquanto o nome andou.

Com key={aluno.matricula}, o React reconhece que o item 2026-014 sumiu, descarta aquela linha inteira e as outras duas seguem com a nota certa.

A moral: a key não é um identificador do dado, é a identidade que o React usa para casar o que existe na tela com o que veio no novo render. Índice não é identidade de aluno — é identidade de posição.

Ordenar a lista faz a mesma coisa

Não precisa remover ninguém. Basta reordenar. Aqui cada linha tem um checkbox de presença, e só a Ana está presente:

jsx
// a mesma Chamada, com um checkbox de presença no lugar do campo de nota
function Chamada({ alunos, usarIndice }) {
  return (
    <ul>
      {alunos.map((aluno, indice) => (
        <li key={usarIndice ? indice : aluno.matricula}>
          <label>{aluno.nome}</label>
          <input type="checkbox" />
        </li>
      ))}
    </ul>
  );
}

const presencas = () =>
  [...document.querySelectorAll('li')]
    .map((li) => {
      const nome = li.querySelector('label').textContent;
      return `${nome}=${li.querySelector('input').checked ? 'presente' : 'ausente'}`;
    })
    .join('  ');

const porNomeDesc = [...turma].sort((a, b) => b.nome.localeCompare(a.nome));

async function rodar(usarIndice) {
  const root = createRoot(document.getElementById('root'));
  await act(() => root.render(<Chamada alunos={turma} usarIndice={usarIndice} />));
  document.querySelectorAll('input')[0].checked = true; // só Ana está presente
  console.log('antes de ordenar:', presencas());

  await act(() => root.render(<Chamada alunos={porNomeDesc} usarIndice={usarIndice} />));
  console.log('ordem Z→A       :', presencas());
  await act(() => root.unmount());
}

Com o índice:

jsx
await rodar(true);
antes de ordenar: Ana Beatriz=presente Caio Ferraz=ausente Diana Prado=ausente ordem Z→A : Diana Prado=presente Caio Ferraz=ausente Ana Beatriz=ausente

Com a matrícula:

jsx
await rodar(false);
antes de ordenar: Ana Beatriz=presente Caio Ferraz=ausente Diana Prado=ausente ordem Z→A : Diana Prado=ausente Caio Ferraz=ausente Ana Beatriz=presente

A professora marcou a Ana, clicou em ordenar, e quem ficou presente foi a Diana. Vale para checkbox do DOM e vale igual para estado de componente feito com useState: o estado mora na posição da árvore que a key determina. Se a key muda de dono, o estado muda de dono junto. Se quiser entender o mecanismo de reconciliação por trás disso, ele está detalhado em por que o componente roda de novo.

O aviso some no build de produção; o bug não

Este é o motivo de não deixar para depois. O aviso de key é uma verificação de desenvolvimento: o React de produção nem executa esse trecho. Salvei num arquivo chamada.jsx o mesmo Chamada e LinhaDeNota do campo de nota — o mesmo roteiro de digitar as três notas e remover a Ana —, só que agora sem key nenhuma, e rodei esse arquivo nos dois builds:

bash
# o build de desenvolvimento — o mesmo transform que o Vite usa em npm run dev
npx esbuild chamada.jsx --format=esm --jsx=automatic --jsx-dev --outfile=dev.mjs
node dev.mjs
Each child in a list should have a unique "key" prop.

Check the render method of Chamada. See https://react.dev/link/warning-keys for more information. digitado: Ana Beatriz=8.5 Caio Ferraz=7.0 Diana Prado=9.5 sem Ana : Caio Ferraz=8.5 Diana Prado=7.0

E o mesmo arquivo com o React de produção — é o NODE_ENV que faz o pacote carregar o outro build, sem nenhuma das verificações de desenvolvimento:

bash
npx esbuild chamada.jsx --format=esm --jsx=automatic --outfile=prod.mjs
NODE_ENV=production node prod.mjs
digitado: Ana Beatriz=8.5 Caio Ferraz=7.0 Diana Prado=9.5 sem Ana : Caio Ferraz=8.5 Diana Prado=7.0

Mesma nota errada nos dois. A única diferença é que em produção ninguém te avisa. O console limpo depois do deploy não significa que o problema sumiu: significa que ele virou um chamado de suporte com o título “a nota do aluno mudou sozinha”.

Quando o índice serve mesmo

Índice não é proibido. Ele é uma identidade correta quando a posição é a identidade — quando a lista nunca muda de ordem nem perde item do meio.

situação da lista índice serve? por quê
estática, renderizada uma vez (menu, colunas fixas) sim a posição nunca muda, logo nunca mente
só cresce no fim (log, mensagens de chat antigas em cima) sim nenhum índice existente é reatribuído
tem remover, filtrar, buscar ou ordenar não a posição muda de dono e o estado vai junto
cada linha tem input, checkbox, foco ou animação não esse estado mora na posição — e a posição escorrega
o item já tem id, uuid, slug ou matrícula não você tem a identidade de verdade na mão, use ela

Na dúvida, use a identidade do dado. Não existe caso em que o índice é melhor que um id estável, então a pergunta prática é sempre “eu tenho um id?” — e não “o índice dá conta aqui?”.

Sem id no dado: como fabricar uma key estável

Às vezes o dado chega sem identificador nenhum. A planilha da secretaria tem só nome e turma. A saída tentadora é compor a key a partir dos campos — e ela funciona até dois alunos terem o mesmo nome:

jsx
const planilha = [
  { nome: 'Ana Beatriz', turma: '9A' },
  { nome: 'Caio Ferraz', turma: '9A' },
  { nome: 'Ana Beatriz', turma: '9A' },
];

function ListaComposta() {
  return (
    <ul>
      {planilha.map((aluno) => (
        <li key={`${aluno.turma}-${aluno.nome}`}>{aluno.nome}</li>
      ))}
    </ul>
  );
}
Encountered two children with the same key, `9A-Ana Beatriz`. 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.

Homônimo em escola não é caso raro, é rotina. A correção é dar um id ao dado no momento em que ele entra no aplicativo — na normalização da resposta da API, uma vez só, e não a cada render:

jsx
// roda uma vez, quando o dado chega
const alunos = planilha.map((aluno) => ({ id: crypto.randomUUID(), ...aluno }));

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

console.log('ids gerados:', alunos.map((a) => a.id.slice(0, 8)).join(', '));
ids gerados: 622a8e82, 9a32dbff, 2ed2b23a

Sem aviso nenhum, e as duas Anas passam a ser duas pessoas diferentes para o React. O detalhe que decide tudo é onde a linha do crypto.randomUUID() mora: fora do componente, junto do dado. Dentro do render, ela vira o problema da última seção.

Fragment dentro de lista também precisa de key

Quando cada item devolve dois elementos irmãos — um <dt> e um <dd>, por exemplo — a vontade é usar o fragmento curto <>. Só que ele não aceita atributo nenhum, key inclusive:

jsx
const alunos = [
  { matricula: '2026-014', nome: 'Ana Beatriz', falta: 2 },
  { matricula: '2026-027', nome: 'Caio Ferraz', falta: 5 },
];

function BoletimCurto() {
  return (
    <dl>
      {alunos.map((aluno) => (
        <>
          <dt>{aluno.nome}</dt>
          <dd>{aluno.falta} faltas</dd>
        </>
      ))}
    </dl>
  );
}
Each child in a list should have a unique "key" prop.

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

A forma longa resolve, e é para isso que ela existe:

jsx
import { Fragment } from 'react';

function BoletimComKey() {
  return (
    <dl>
      {alunos.map((aluno) => (
        <Fragment key={aluno.matricula}>
          <dt>{aluno.nome}</dt>
          <dd>{aluno.falta} faltas</dd>
        </Fragment>
      ))}
    </dl>
  );
}
<dl><dt>Ana Beatriz</dt><dd>2 faltas</dd><dt>Caio Ferraz</dt><dd>5 faltas</dd></dl>

Nenhum aviso, e o <Fragment> não deixa marca no HTML: os <dt> e <dd> são filhos diretos do <dl>, como deviam ser.

Por que Math.random() como key é pior que o índice

Essa é a “solução” que aparece em resposta de fórum: se a key precisa ser única, gere uma aleatória e o aviso some. Some mesmo. E aí você troca um bug intermitente por um bug permanente.

jsx
function Chamada({ filtro, comRandom }) {
  const visiveis = turma.filter((a) => a.nome.includes(filtro));
  return (
    <ul>
      {visiveis.map((aluno) => (
        <li key={comRandom ? Math.random() : aluno.matricula}>
          <label>{aluno.nome}</label>
          <input type="text" defaultValue="" />
        </li>
      ))}
    </ul>
  );
}

O teste: digitar a nota na primeira linha e disparar um render qualquer — no caso, a professora digitando no campo de busca sem mudar o resultado. Guardei o nó <li> antes e comparei com o de depois usando ===:

jsx
const primeiroLi = () => document.querySelector('li');

async function rodar(comRandom) {
  const root = createRoot(document.getElementById('root'));
  await act(() => root.render(<Chamada filtro="" comRandom={comRandom} />));

  const noAntes = primeiroLi();
  document.querySelector('input').value = '8.5';
  console.log('nota digitada na 1a linha:', document.querySelector('input').value);

  // novo render, exatamente a mesma lista
  await act(() => root.render(<Chamada filtro="" comRandom={comRandom} />));

  console.log('mesmo nó <li> depois do re-render?', noAntes === primeiroLi());
  console.log('nota que sobrou na 1a linha    :', JSON.stringify(document.querySelector('input').value));
  await act(() => root.unmount());
}

Com a key aleatória:

jsx
await rodar(true);
nota digitada na 1a linha: 8.5 mesmo nó <li> depois do re-render? false nota que sobrou na 1a linha : ""

Com a matrícula, o mesmo roteiro:

jsx
await rodar(false);
nota digitada na 1a linha: 8.5 mesmo nó <li> depois do re-render? true nota que sobrou na 1a linha : "8.5"

O false da primeira saída é a prova: com Math.random() o <li> é um elemento novo em folha a cada render, ou seja, o React destrói a linha inteira e monta outra. A nota digitada evaporou, e o mesmo aconteceria com o foco do teclado, com o texto selecionado e com qualquer estado interno da linha.

O índice pelo menos erra de vez em quando — só quando a lista muda. O Math.random() erra sempre, inclusive quando nada mudou. E como o valor é diferente a cada chamada, ele também apaga qualquer chance de o React reaproveitar trabalho. Se quiser ver como essa função se comporta, Math.random na prática mostra o que ela devolve. O que ela não devolve é identidade.

O que fazer agora

Abra o console, procure o nome que vem depois de Check the render method of e vá naquele componente. Se o item já tem id ou matrícula, o conserto é de um caractere. Se não tem, gere o id na normalização do dado, não no render.

Confirme a correção com uma lista que tenha um <input> por linha: digite na segunda linha, remova a primeira e depois ordene os itens. Com key={id}, o texto e o foco devem continuar ligados ao mesmo registro. Repita com key={index} para observar a identidade acompanhar a posição. Esse antes e depois prova o problema melhor do que apenas fazer o aviso sumir.

Depois disso, o próximo lugar onde a key costuma morder é o formulário: campo controlado dentro de lista junta os dois assuntos deste artigo. A lição de formulário controlado no React mostra como o value e o onChange se ligam ao estado, e map, filter e reduce recapitula o método que gera a lista em primeiro lugar. O caminho completo de estudo, na ordem, está no guia de React e na trilha de React.

  • react
  • key
  • listas
  • warning
  • map

Perguntas frequentes

O aviso de key quebra a minha aplicação?
Na primeira renderização, não: a lista aparece inteira e correta. O estrago acontece na próxima vez que a lista muda de tamanho ou de ordem, porque aí o React precisa da key para saber qual linha é qual.
Posso usar o índice do map como key?
Pode, quando a lista nunca é reordenada, filtrada nem tem item removido do meio, e nenhuma linha guarda estado próprio. Fora disso o índice é uma identidade falsa: ele descreve a posição, não o item.
A key precisa ser única no mundo todo?
Não. Ela só precisa ser única entre os irmãos da mesma lista. Duas listas diferentes na mesma tela podem usar a key "1" sem nenhum conflito.
Por que props.key vem undefined dentro do componente?
Porque key não é uma prop: o React tira essa chave do objeto antes de montar as props e guarda para si. Se o componente precisa do valor, passe de novo com outro nome, como matricula ou id.
Dá para desligar esse aviso?
Dá, e é péssima ideia. O aviso já não aparece no build de produção — ele é exatamente o momento em que dá para corrigir de graça. Silenciar no desenvolvimento é apagar o único alerta que você teria.

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 19.2.8, 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. React — Fragment (keyed fragments) — react.dev

Continue por aqui