beginner19 min

Conditional Rendering, Lists, and Stable Keys

Rendering different UI based on a condition, rendering a list from an array, and the one rule about list keys that prevents real, hard-to-diagnose bugs.

What you'll learn

  • Render different UI conditionally using expressions, not statements
  • Explain why array index is an unsafe key for a reorderable or filterable list
  • Design a stable, unique key extraction strategy for real data

Prerequisites

Explanation

From the JSX lesson: curly braces only accept expressions, not statements, because they're arguments to a function call. That's why conditional rendering in JSX leans on expressions — the ternary operator (condition ? <A /> : <B />) for either/or, and the && operator (condition && <A />) for render-this-or-render-nothing — rather than an if statement, which produces no value at all.

Rendering a list from an array is the array's own .map(), nothing React-specific: items.map(item => <ItemRow key={item.id} {...item} />) transforms an array of data into an array of elements, exactly the way .map() transforms any array. What is React-specific is the key prop, and it's not optional decoration — it's how React matches each element in a new render to the corresponding element from the previous render, so it can correctly preserve, update, or remove exactly the right one.

Array index as a key looks fine until the list reorders, filters, or has an item inserted/removed from the middle — then it silently breaks. If a list of [A, B, C] (keyed 0, 1, 2) has A removed, the new list [B, C] is keyed 0, 1 — meaning React sees "key 0's content changed from A to B" and "key 1's content changed from B to C," not "the first item was removed." For static, decorative content this rarely matters. For a list with per-item local state (a controlled input inside each row, an expanded/collapsed toggle) it's a real, confusing bug: after removing item A, item B's row can end up displaying A's leftover local state, because React matched the wrong slot to the wrong data.

The fix is a stable identity that travels with the data itself — a database id, a UUID generated once when the item was created, anything that uniquely and permanently identifies that specific item regardless of its position in the array. If your data genuinely has no natural id, generating and storing one when the item is created (not deriving one from its current array position) is the correct fix — not reaching for index as a shortcut.

Example

Modeling why index-based keys break identity tracking when a list changes shape — before, after removing the first item.

const before = ["Alice", "Bob", "Carol"];
const beforeKeyed = before.map((name, index) => ({ key: index, name })); // BUGGY: index as key

const after = ["Bob", "Carol"]; // Alice removed
const afterKeyed = after.map((name, index) => ({ key: index, name }));

console.log(beforeKeyed[0]); // { key: 0, name: "Alice" }
console.log(afterKeyed[0]);  // { key: 0, name: "Bob" } -- SAME key, different person!

Try it yourself

Fix the keying by using each person's actual id instead of the array index, then re-run.

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Guided exercise

Guided exercise

Write a function findUnsafeIndexKeys(list) that returns true if `list` (an array of objects) has NO stable id field (no `id` property on its items, meaning index-based keying would be the only option), false if every item has a usable `id`.

Checks: does not flag a list with stable ids · flags a list with no stable ids

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Independent exercise

Independent exercise

Write a function keyExtractor(list) that returns an array of stable keys: use each item's `id` if present, otherwise generate one as `'generated-' + index` (a documented, deliberate fallback — not a silent unsafe default) as a last resort.

Checks: uses real ids when present · falls back to a labeled generated key when no id exists

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Common mistakes

  • Using `if` statements inline in JSX instead of a ternary or `&&` expression, since curly braces only accept expressions.
  • Using the array index as a key for any list that can reorder, filter, or have items inserted/removed from the middle.
  • Assuming index-based keys are "fine for now" without checking whether the list items carry any per-item local state that could get silently mismatched.

Knowledge check

Knowledge check

1. Why does `condition && <Component />` work in JSX but `if (condition) { <Component /> }` does not?
2. What specifically breaks when array index is used as a key and an item is removed from the middle of the list?
3. What makes a key genuinely "stable" for a list item?

Takeaway

Conditional rendering uses expressions because JSX curly braces only accept values, not statements — and list keys must be tied to an item's identity, not its position, or React can silently misattribute state to the wrong element.

Summary

This lesson covered expression-based conditional rendering and demonstrated concretely why array-index keys break when a list's shape changes, and how to design a stable key-extraction strategy.

References

Your notes

Notes save automatically.

Finished this lesson?

Mark it complete to track your progress and schedule a future review.