Skip to content

React · Components

React Lists and Keys — A Complete Interview Guide

7 min read Updated 2026-06-23 Share:

Practice Lists and Keys interview questions

Rendering lists in React

The standard pattern is Array.map — transform a data array into an array of React elements, which React renders as children.

function ProductList({ products }) {
  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          <strong>{product.name}</strong> — ${product.price}
        </li>
      ))}
    </ul>
  )
}

Every element returned from .map() must have a key prop. Without it, React logs a warning in development and falls back to positional matching — which produces incorrect output whenever the list changes.

What the key prop actually does

key is a special React prop — not forwarded to the component, not readable via props.key — that exists solely for React's reconciler. It gives each element in a list a stable identity.

During reconciliation, React builds a map of the previous render's list keyed by key. When the new render arrives, React looks up each element's key in that map:

  • Same key, same type → update props in place (potentially reorder the DOM node).
  • New key → mount a new component.
  • Key gone → unmount the old component.

Without keys, React matches elements by position: old index 0 = new index 0. This fails as soon as items are inserted, deleted, or reordered.

The index-as-key bug

This is one of the most frequently tested React correctness questions.

Using array index as a key breaks when the list can be sorted, filtered, or have items added or deleted:

// ❌ Index keys — dangerous for mutable lists
{todos.map((todo, i) => <TodoItem key={i} todo={todo} />)}

The scenario that breaks: you have three items [Alice, Bob, Carol]. You delete Alice. The list becomes [Bob, Carol]. With index keys:

  • Key 0 was Alice → now key 0 is Bob. React thinks the Alice component (now at index 0) received new props for Bob. React updates the props but keeps Alice's state.
  • Controlled input showing Alice's draft text now shows Bob's name but still contains Alice's partially-typed message.
  • Animated entrance effects trigger for Bob and Carol instead of Alice leaving.

The fix:

// ✅ Stable id from the data
{todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}

What makes a good key

  1. Unique among siblings — within a single list, no two siblings share a key. The same key can appear in a different list.
  2. Stable — the same data item has the same key on every render.
  3. String or number — React converts numbers to strings internally.
// ✅ Database primary key
<Row key={row.id} />

// ✅ Slug, if guaranteed unique within the list
<Tab key={tab.slug} />

// ✅ Composite key when no single field is unique
<Cell key={`${row.id}-${col.name}`} />

// ❌ Math.random() — new key every render → unmount/remount on every render
<Item key={Math.random()} />

// ❌ Array index for a mutable list
<Item key={i} />

Generating keys inside .map() with Math.random() or Date.now() is worse than no key at all — it forces React to unmount and remount every item on every render, discarding all state and running all effects twice.

When index keys are safe

The React team's guidance: three conditions must all be true.

  1. The list is static — no insertions, deletions, or reorderings.
  2. Items have no stable IDs in the data.
  3. Items have no internal state — they are pure display.
// Acceptable: static ordered list, no ids, no state
const STEPS = ['Install dependencies', 'Configure environment', 'Run migrations']
{STEPS.map((step, i) => <li key={i}>{step}</li>)}

When in doubt, the data should have IDs. Generate them at the source, not at render time.

Where to place the key

key goes on the outermost element returned from the .map() callback:

// ✅ key on the outer element
{items.map(item => (
  <ProductCard key={item.id} item={item} />
))}

// ❌ key on an inner element — React doesn't see it for list tracking
{items.map(item => (
  <div>
    <ProductCard key={item.id} item={item} />  {/* wrong */}
  </div>
))}

Fragments in lists

When each list item needs to render multiple sibling elements without a wrapper DOM node, use <React.Fragment key={id}>. The short syntax <> doesn't accept a key prop.

function DefinitionList({ terms }) {
  return (
    <dl>
      {terms.map(term => (
        <React.Fragment key={term.id}>
          <dt>{term.word}</dt>
          <dd>{term.definition}</dd>
        </React.Fragment>
      ))}
    </dl>
  )
}

Using <div> instead would produce <dl><div><dt>…</dt><dd>…</dd></div></dl>, which is invalid HTML. The Fragment produces <dl><dt>…</dt><dd>…</dd></dl> — structurally correct.

No ID in the data?

Generate IDs when the data enters the app, not at render time:

// When you receive data (e.g., in a reducer or fetch handler)
const items = rawItems.map(item => ({
  ...item,
  id: crypto.randomUUID(),   // stable across renders — generated once
}))

Or use a composite key from fields that together are unique within the list:

{countries.map(c => (
  <CountryRow key={`${c.code}-${c.region}`} country={c} />
))}

Handling dynamic list types

When a list can contain items of different component types, use an object map to select the component:

const BLOCK_MAP = {
  text:    TextBlock,
  image:   ImageBlock,
  code:    CodeBlock,
  divider: DividerBlock,
}

function ContentArea({ blocks }) {
  return (
    <>
      {blocks.map(block => {
        const Block = BLOCK_MAP[block.type]
        if (!Block) return null
        return <Block key={block.id} {...block} />
      })}
    </>
  )
}

Note: Block must be uppercase. JSX treats a lowercase tag as a DOM element string; it treats an uppercase name as a JavaScript variable reference.

Nested lists

Each nesting level is its own sibling context. Keys at one level don't interact with keys at another level. Every .map() at every level needs its own key.

function Table({ rows }) {
  return (
    <table>
      <tbody>
        {rows.map(row => (
          <tr key={row.id}>
            {row.cells.map(cell => (
              <td key={cell.colId}>{cell.value}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  )
}

row.id and cell.colId are scoped to their own lists. No namespace collision.

Reconciliation deep dive

Here's what React does internally when a list updates:

Before: [Apple(key=1), Banana(key=2), Cherry(key=3)]
After:  [Cherry(key=3), Apple(key=1), Banana(key=2)]

With stable keys:
  React maps old keys: {1: Apple-fiber, 2: Banana-fiber, 3: Cherry-fiber}
  For new key=3 at index 0: fiber exists → move Cherry's DOM node to index 0
  For new key=1 at index 1: fiber exists → move Apple's DOM node to index 1
  For new key=2 at index 2: fiber exists → move Banana's DOM node to index 2
  Result: 3 DOM moves, 0 unmounts, 0 mounts

Without keys (positional):
  Index 0: Apple → Cherry. React updates props. 1 update.
  Index 1: Banana → Apple. React updates props. 1 update.
  Index 2: Cherry → Banana. React updates props. 1 update.
  Result: 3 prop updates — correct for display, but state stays at old position.

Keys make reordering O(n) and correct. Without them, reordering is O(n) in mutations but incorrect for stateful components.

What interviewers are testing

Keys questions check whether you understand:

  1. Why keys are required, not just that they are required.
  2. The correctness bug with index keys on mutable lists.
  3. What makes a key "good" (unique among siblings, stable, never generated at render time).
  4. Where to place the key (outermost element of the map callback).
  5. The React.Fragment keying pattern for multi-element list items.

The follow-up question is almost always the index bug scenario. Be ready to describe exactly what goes wrong with a concrete example — state staying at the wrong position while props update.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel