← React & TypeScript

React Fundamentals

What are keys for when you render a list in React, and why isn't using the index recommended?

Show answer — try answering out loud first

A key is a stable identifier that tells React which list item is which from one render to the next. With correct keys, React knows whether an item moved, was added, or was removed. Using the index as a key fails when the list is reordered, filtered, or has items inserted, because the index changes owners.

In React, lists are rendered with map inside the JSX: you transform an array of data into an array of elements, and each one needs a key prop. Why? When the list changes, React compares the new list with the previous one; without a clear identity, it can only compare by position. The key gives each element a real identity:

  • Stable and unique key (an id from the data): React correctly matches each element even if it changes position. It preserves its state (inputs, focus, animations) and just moves the node.
  • Index as key: the key describes the position, not the data. If you remove the first item, all the others "inherit" the previous one's index and React thinks only the content changed. The classic symptom: you delete a row and an input's text "jumps" to the wrong row.

Rule of thumb: use the data's id. The index is only acceptable if the list is static (never reordered, filtered, or inserted into) and its items have no state.

import { useState } from "react";

type Tarea = { id: string; titulo: string }; // the id is the stable identity

function ListaTareas() {
  const [tareas, setTareas] = useState<Tarea[]>([
    { id: "a1", titulo: "Estudiar hooks" },
    { id: "b2", titulo: "Practicar TypeScript" },
  ]);

  const eliminar = (id: string) =>
    setTareas((prev) => prev.filter((tarea) => tarea.id !== id));

  return (
    <ul>
      {/* map turns data into elements; each one carries its key */}
      {tareas.map((tarea) => (
        <li key={tarea.id}>
          {tarea.titulo}
          {/* The input keeps its value: the key identifies the task */}
          <input placeholder="notas" />
          <button onClick={() => eliminar(tarea.id)}>Eliminar</button>
        </li>
      ))}
    </ul>
  );
}

// Behavior: type "hola" in the second task's notes and delete
// the first one. With key={tarea.id}, "hola" stays in its row; with the index it doesn't.

Using the map index as a key in a list that changes:

// BAD: the key is the position, not the data's identity.
// If you remove task 0, the one that was 1 now has key 0: React reuses
// the old <li> and the input keeps the wrong text.
{tareas.map((tarea, indice) => (
  <li key={indice}>{tarea.titulo}<input placeholder="notas" /></li>
))}
// GOOD: the key travels with the data even if its position changes
{tareas.map((tarea) => (
  <li key={tarea.id}>{tarea.titulo}<input placeholder="notas" /></li>
))}

If your data doesn't come with an id, generate one when you create it (for example with crypto.randomUUID()), never during the render.

"Keys give each list item an identity so React can match them across renders. With a stable key, like the data's id, React knows whether an item moved, was inserted, or was removed, and it preserves its state and its DOM node. The index as a key is dangerous because it represents the position: if the list is reordered or I filter items, the indexes change owners and bugs appear, like inputs whose text jumps to the wrong row. That's why I use the data's id and would only accept the index in fully static lists."

Quick challenge

This component has a key bug: if you check the checkbox of the first product and then delete it, the check "jumps" to the next one. Find the problem and fix it.

type Producto = { id: number; nombre: string };
function Carrito({ productos }: { productos: Producto[] }) {
  return (
    <ul>
      {productos.map((producto, i) => (
        <li key={i}><input type="checkbox" /> {producto.nombre}</li>
      ))}
    </ul>
  );
}
See solution
function Carrito({ productos }: { productos: Producto[] }) {
  return (
    <ul>
      {productos.map((producto) => (
        // The key is now the product's identity, not its position.
        // When an item is removed, each checkbox stays with its product.
        <li key={producto.id}><input type="checkbox" /> {producto.nombre}</li>
      ))}
    </ul>
  );
}