LLevelUp
0
← Back to topic

Lists & Keys

Rendering lists is one of the most common tasks in React UI work. React’s reconciliation algorithm uses key props to efficiently update lists — getting keys wrong leads to subtle bugs and broken animations.

Rendering Lists with .map()

The standard way to render a list in React is to call .map() on an array inside JSX. Each call to .map() should return a JSX element.

type Task = { id: string; title: string; done: boolean };

function TaskList({ tasks }: { tasks: Task[] }) {
  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id} className={task.done ? "done" : ""}>
          {task.title}
        </li>
      ))}
    </ul>
  );
}

The key prop must be placed on the outermost element returned from map — in this case the <li>, not any element inside it.

Why Keys Are Required

When React re-renders a list, it needs to know which items changed, which were added, and which were removed. Without keys, React compares elements by their position in the array — any reorder, insertion, or deletion forces a full re-render of all subsequent items.

With keys, React identifies each element by a stable identity and only updates the DOM nodes that actually changed.

sequenceDiagram
  participant State as State (old)
  participant React as React Reconciler
  participant DOM as DOM

  State->>React: tasks = [A, B, C]
  React->>DOM: Render A(key=1), B(key=2), C(key=3)
  Note over State: User deletes B
  State->>React: tasks = [A, C]
  React->>DOM: Remove B(key=2) — A and C untouched

Without keys, deleting B would cause React to update B’s DOM node to look like C and remove the last node — wasteful and incorrect for stateful elements (inputs, animations).

Choosing Good Keys

Keys must be:

  • Unique among siblings (not globally unique)
  • Stable across renders
  • Not based on render-time computations (like Math.random())
// Good: database IDs are stable and unique
tasks.map(task => <TaskRow key={task.id} task={task} />)

// Good: slug from content
posts.map(post => <PostCard key={post.slug} post={post} />)

// Problematic: see below
tasks.map((task, index) => <TaskRow key={index} task={task} />)

When Index Keys Cause Bugs

Using array index as a key is only safe when the list is static (no reorders, no inserts, no deletes) and items have no state of their own.

The bug appears when items are reordered or inserted:

// If you insert a task at position 0, every key shifts by 1
// React reuses the DOM node at position 0 but assigns new props —
// any uncontrolled input inside that node retains the old value
tasks.map((task, index) => <TaskRow key={index} task={task} />)

CAUTION

If list items contain inputs, animations, or any component state, and the list can be sorted or filtered, index keys will produce incorrect behavior. Always prefer a stable data ID.

Filtering and Transforming Before Rendering

You can chain array methods before .map() to filter or sort:

function ActiveTaskList({ tasks }: { tasks: Task[] }) {
  return (
    <ul>
      {tasks
        .filter(task => !task.done)
        .sort((a, b) => a.title.localeCompare(b.title))
        .map(task => (
          <li key={task.id}>{task.title}</li>
        ))}
    </ul>
  );
}

NOTE

.sort() mutates the original array. If tasks comes from state, spread it first: [...tasks].sort(...). Mutating state directly bypasses React’s change detection.

Rendering Lists of Components

The same pattern applies when each item renders a full component:

function ProductGrid({ products }: { products: Product[] }) {
  return (
    <div className="grid">
      {products.map(product => (
        <ProductCard
          key={product.id}
          name={product.name}
          price={product.price}
          imageUrl={product.imageUrl}
        />
      ))}
    </div>
  );
}

The key is used by React internally — it is not passed as a prop to ProductCard. If you need to access it inside the component, pass it explicitly as a separate prop.

Further Learning

Search these terms to go deeper:

  • “React reconciliation keys react.dev” — the official explanation of how React’s diffing algorithm uses keys
  • “why index as key is an antipattern Robin Pokorny” — the original article explaining the bug with index keys
  • “React list rendering performance” — when large lists need virtualization (react-window, TanStack Virtual)
  • “Kent C. Dodds React key prop” — using key as a reset mechanism (intentional remounting)
  • “react-window react-virtual large lists” — libraries for efficiently rendering thousands of items