Rendering Lists and Understanding Keys

Learn how to render arrays of data with .map(), why React needs stable keys to track list items across re-renders, and how key choice affects component identity, state preservation, and remounting.

Rendering a list — a feed of posts, a table of rows, a set of form fields — almost always means calling .map() over an array of data to produce an array of JSX elements. React needs a way to tell which rendered item corresponds to which piece of data across re-renders, and that's exactly the job of the key prop.

Rendering arrays with .map()

Because JSX is just JavaScript expressions, an array of JSX elements is valid content anywhere a single element would be — React renders each element of the array in order. The most common way to produce that array is items.map(item => <li key={item.id}>{item.name}</li>), transforming each data item into a corresponding element.

The callback passed to .map() receives the item, its index, and the full array, matching the signature of Array.prototype.map in plain JavaScript. In React specifically, the returned JSX must include a special key prop so React can efficiently match old elements to new ones between renders.

It's common to filter, sort, or transform the underlying data before mapping it to JSX — for example items.filter(i => i.active).map(i => <Row key={i.id} item={i} />) — keeping the data-shaping logic separate from the rendering logic for readability.

Why keys matter: the diffing algorithm

When React re-renders a list, it needs to reconcile the new array of elements against the previous one to decide which DOM nodes to keep, update, move, or discard. Without keys, React would have to guess based on position alone, which breaks down badly whenever items are inserted, removed, or reordered anywhere but the very end.

The key prop gives React a stable identity for each element that survives across renders regardless of position. When React sees the same key in both the old and new list, it matches those two elements together, reuses the existing DOM node and component instance, and only updates the props that actually changed — instead of unmounting and remounting everything.

Keys only need to be unique among siblings in that specific list, not globally unique across the whole app. They also must be stable — the same logical item should get the same key on every render — which is exactly why keys should come from your data (like a database id) rather than being generated fresh on every render.

Index keys, reordering, and state preservation

Using the array index as a key (key={index}) is convenient and works fine when the list is static and never reordered, filtered, or has items inserted/removed from the middle. But the moment items can move, index keys become actively misleading: item at index 2 keeps the key 2 even after the underlying data item there has changed, so React reuses the wrong component instance for the wrong data.

This becomes a real bug, not just a performance issue, whenever list items hold their own internal state — for example an input field per row, an 'expanded' toggle, or a checked checkbox. If you delete the first item in a list keyed by index, React sees that index 0 still exists (now holding what used to be item 1's data) and reuses that component instance, including its internal state, which visually appears as if the wrong row's checkbox got checked or the wrong row's input kept its typed text.

The fix is to key list items by something stable and unique to the underlying data — usually a database id, a UUID, or another naturally unique field — so that when an item moves, is inserted, or is removed, React's diffing correctly matches components to the data they actually represent, both for correct DOM updates and correct state preservation.

Forcing a remount with key

Because key controls component identity, deliberately changing an element's key is a technique for forcing React to unmount the old component instance and mount a completely fresh one, discarding all of its internal state. This is useful when you want a component to fully reset — for example, giving a form component key={selectedUserId} so switching to edit a different user starts the form with fresh, blank internal state rather than the previous user's leftover form state.

This key-based remount trick works because React's identity rule is: same type + same key at the same position means 'update the existing instance'; a different key (or different type) at that position means 'discard the old instance and create a new one'. Understanding this rule explains both why stable keys preserve state correctly during reordering, and why changing a key on purpose is a valid way to intentionally reset a component.

Mapping an array to keyed JSX

item.id should be a stable, unique-per-list value drawn from your data — not the array index and not a freshly generated value on every render. The key prop is stripped out by React before reaching the component as a regular prop; access it via a separate prop like id if the component needs that value internally.

items.map(item => (
  <li key={item.id}>{item.name}</li>
))

Code Examples

Each rendered <li> gets a key drawn from the item's own id field, giving React a stable identity for each row independent of its position in the array. Expected result: Renders one <li> per item, in array order, each tagged internally with its id as the React key.

<li key={item.id}>{item.name}</li> TodoRow keeps its own local isEditing state. Keying each TodoRow by todo.id (rather than its index in the array) ensures that when todos are reordered, inserted, or deleted, React matches each TodoRow instance — and its in-progress edit state — to the correct underlying todo. If this list used key={index} instead, deleting the first todo would shift every remaining todo up one index, causing React to reuse each row's component instance (and its isEditing state) for the wrong todo.

Common mistake: Using todos.map((todo, index) => <TodoRow key={index} todo={todo} />) would cause an in-progress edit to visually 'jump' to a different row whenever the list is reordered or an earlier item is removed.

interface Item {
  id: string;
  name: string;
}

function ItemList({ items }: { items: Item[] }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}
interface Todo {
  id: string;
  text: string;
  done: boolean;
}

function TodoRow({ todo }: { todo: Todo }) {
  const [isEditing, setIsEditing] = useState(false);
  // Each row's own isEditing state should stay tied to THIS todo,
  // even as todos above it are added, removed, or reordered.
  return (
    <li>
      {isEditing ? (
        <input defaultValue={todo.text} />
      ) : (
        <span onClick={() => setIsEditing(true)}>{todo.text}</span>
      )}
    </li>
  );
}

function TodoList({ todos }: { todos: Todo[] }) {
  return (
    <ul>
      {todos.map(todo => (
        // Keying by todo.id (not index) keeps each row's isEditing
        // state correctly attached to the right todo after reordering.
        <TodoRow key={todo.id} todo={todo} />
      ))}
    </ul>
  );
}

Index keys vs stable id keys after deleting the first item

Index keys shift when items are removed, causing React to reuse the wrong component instance's state; stable id keys travel with the data.

Before delete:            After deleting "Apple":

key=0  Apple                key=0  Banana   <- WRONG: reused key 0's
key=1  Banana                                  state, now shows Banana
key=2  Cherry               key=1  Cherry      but with Apple's old
                                                 component state

--- versus stable id keys ---

key=a1 Apple                key=b2  Banana   <- correct: each id keeps
key=b2 Banana                                   its own component
key=c3 Cherry                key=c3  Cherry     instance and state

Important Rules

Always provide a key when rendering a list with .map(), placed on the outermost element returned for each item.

Keys must be unique among siblings in that list, and stable across re-renders for the same logical item.

Prefer a stable data field (id, uuid) as the key over the array index whenever the list can be reordered, filtered, or have items inserted/removed from the middle.

Index keys are acceptable only for lists that are static: never reordered, and never have items added or removed except at the end.

Deliberately changing a component's key is a valid technique to force a full remount and reset its internal state.

Where this shows up

Feeds, tables, kanban boards, shopping carts, and any drag-and-drop reorderable list all depend on correct keys to keep the right row's checkbox checked, the right card's expanded state open, and the right input's focus and typed text attached to the right underlying item as data is added, removed, filtered, or reordered.

todos.map((todo, index) => <TodoRow key={index} todo={todo} />) todos.map(todo => <TodoRow key={todo.id} todo={todo} />)

Index keys tie a component instance to a position, not to the underlying data item, so reordering or deleting items causes React to reuse the wrong instance's internal state for the wrong data.

items.map(item => <li key={Math.random()}>{item.name}</li>) items.map(item => <li key={item.id}>{item.name}</li>)

Generating a new random key on every render means the key is never stable across re-renders, so React treats every item as brand new on every render, unmounting and remounting the whole list and destroying any per-item state or focus.

[multiple-choice] Why does React need a key prop when rendering a list with .map()? A. To give React a stable identity for each item so it can correctly match old and new elements across re-renders B. To improve the CSS styling of list items C. It is required by JavaScript's Array.prototype.map method D. To automatically sort the list alphabetically

A. To give React a stable identity for each item so it can correctly match old and new elements across re-renders — Keys let React's reconciliation algorithm match elements between renders by identity rather than by position, so it can correctly reuse, update, move, or discard the right component instances.

[multiple-choice] What problem specifically arises from using the array index as a key in a reorderable or filterable list? A. Component instances and their internal state can get reused for the wrong underlying data item B. The list will render in reverse order C. TypeScript will refuse to compile the component D. The list will re-fetch data from the server on every render

A. Component instances and their internal state can get reused for the wrong underlying data item — Because index keys are tied to position rather than to the data item itself, inserting, removing, or reordering items shifts which data sits at each index, causing React to reuse an existing component instance's state for a different, mismatched item.

[true-false] True or false: keys must be globally unique across the entire application. A. True B. False

B. False — False. Keys only need to be unique among sibling elements within the same list — the same key value can safely be reused in a completely different list elsewhere in the app.

Given an array of comment objects each with a unique commentId field, write the JSX that renders them as a list of <CommentCard> components with correct keys.

comments.map(comment => ( <CommentCard key={comment.commentId} comment={comment} /> ))

Use comment.commentId as the key, not the array index.