A key nas listas: porque é que o índice quase nunca serve
A key não existe para o React ser rápido. Existe para ele responder a uma pergunta: este elemento é o mesmo de antes, ou é outro? Entre dois renders há duas listas, e a key é a única coisa que liga os elementos de uma aos da outra.
Quando se usa o índice como key, o que se está a dizer é «o terceiro continua a ser o terceiro». Isso é ordem, não identidade — e as duas coisas deixam de coincidir mal a lista mude.
1. O caso que se vê a olho nu
{tarefas.map((tarefa, i) => (
<li key={i}>
<input type="checkbox" />
{tarefa.texto}
</li>
))}Marque-se a caixa da segunda tarefa e apague-se a primeira. A lista passa a ter menos um elemento, e o React compara chaves: o 0 continua a existir, o 1 continua a existir. Para ele não desapareceu nada — apenas mudou o texto de cada linha.
O resultado é que a caixa marcada fica onde estava, agora ao lado de outra tarefa. Ninguém mexeu nela; ela é que nunca foi considerada parte da linha que se apagou.
Com uma chave estável, o React vê que a linha desapareceu e desmonta-a com o que ela tinha dentro:
{tarefas.map((tarefa) => (
<li key={tarefa.id}>
<input type="checkbox" />
{tarefa.texto}
</li>
))}2. Onde é que isto dói mais
Em tudo o que o React não controla e que vive no DOM: o valor de um campo não controlado, a posição do scroll, o foco, uma animação a meio, um vídeo a tocar. Tudo isso fica agarrado ao nó que foi reaproveitado, e o nó foi reaproveitado por causa da chave.
Numa lista só de texto, o índice não parte nada. Assim que houver estado por baixo, parte — e parte de uma forma difícil de atribuir, porque o erro aparece numa linha em que ninguém tocou.
3. Quando o índice serve mesmo
Serve, e há um caso deste site que ilustra bem a diferença. A paginação constrói uma lista que mistura números de página com reticências:
{paginas.map((p, i) =>
p === '…' ? (
<span key={`e${i}`}>…</span>
) : (
<Link key={p} href={endereco(p)}>{p}</Link>
)
)}Repare nas duas chaves. A página usa o próprio número, que é a identidade dela — a página 7 é a página 7, apareça onde aparecer. As reticências usam o índice, porque umas reticências não têm identidade nenhuma: são um caractere, iguais entre si, e não guardam nada.
Daí a regra que substitui o «nunca use o índice»: o índice serve quando o elemento não tem identidade própria, não tem estado, e não contém nada que tenha estado. Falhando uma das três, não serve.
4. E quando não há id nenhum?
Primeiro, procurar melhor. Quase sempre há um campo que já é único — um slug, um caminho, um endereço de correio. Neste site as chaves são tag.id, rota.caminho, coluna.chave, opcao.valor: nenhuma foi inventada, todas já lá estavam.
Se a lista é construída no browser e os itens nascem sem identificador, gera-se um quando o item é criado:
// Sim: o id nasce com o item e acompanha-o
function acrescentar(texto) {
setTarefas([...tarefas, { id: crypto.randomUUID(), texto }]);
}
// Não: chave nova a cada render — o React deita tudo fora e volta a montar
<li key={crypto.randomUUID()}>Aquele segundo caso é pior do que o índice. Com o índice, o React reaproveita o que não devia; com uma chave nova de cada vez, ele destrói e reconstrói a lista inteira a cada render — e leva com ela o foco, o scroll e o que lá estivesse escrito.
5. Dois pormenores que passam despercebidos
A chave vai no elemento de fora do
map, não num filho. Pô-la no<input>em vez de no<li>não dá erro nenhum e não serve para nada.Só precisa de ser única entre irmãos, não no site todo. Duas listas diferentes podem usar as mesmas chaves sem problema — é um mal-entendido que leva muita gente a construir chaves compridas sem necessidade.
6. O outro lado: mudar a chave de propósito
Tudo isto tem um uso deliberado. Se a chave mudar, o React deita o componente fora e monta um novo — com o estado todo limpo. É a forma mais simples de repor um formulário quando se muda de registo, e já lá tinha chegado por outro caminho, no artigo dos efeitos que não são precisos.
O resumo
A key responde a «é o mesmo?». O índice responde a «está na mesma posição?». Enquanto a lista não mudar, as duas respostas coincidem — e é por isso que o erro só aparece no dia em que alguém apaga uma linha do meio.