Topic #17Core16 min read

Rendering Patterns (Lists, Keys, Conditionals)

The patterns you use on every screen: rendering lists with .map(), why keys exist and why the index is dangerous, conditional rendering with ternary/&&/early-return and the 0 pitfall, empty and loading states, Fragments in lists, and how keys drive reconciliation and preserve state.

#react#lists#keys#conditional-rendering#reconciliation#fragments#virtualization#empty-state

Rendering a list. You render a collection by mapping an array to an array of elements: items.map(item => <li key={item.id}>{item.label}</li>). JSX accepts an array of elements as children, so the map result drops straight into your markup. This transform-then-render pattern is how every table, feed, and menu is built in React.

Why keys exist. Every element in a list needs a key — a string or number that is stable and unique among siblings. Keys are React's way of giving each list item an identity so that, during reconciliation, it can match items between the old and new render: which stayed, which were added, which were removed, which moved. Without stable identity, React can only match by position.

Keys preserve state and DOM (the real reason). Because keys establish identity, React reuses the same component instance and DOM node for an item that keeps its key across renders — preserving its state (input text, focus, scroll, animation) and avoiding needless DOM churn. Change an item's key and React throws away the old instance and mounts a fresh one. So keys aren't just a warning-silencer; they control component lifecycle within lists.

Never use the array index as a key for dynamic lists. The index is positional, not identity. If items are inserted, removed, reordered, or filtered, the same index now points to a different item — so React reuses the wrong instance and DOM node, causing subtle bugs: a checkbox stays checked on the wrong row, an input holds another item's text, animations glitch. Use a real domain id (item.id). Index keys are only acceptable for a static list that never changes order or length.

Where the key goes. Put the key on the outermost element returned by the map callback, not on inner elements. When each item needs multiple sibling elements without a wrapper, use <React.Fragment key={id}>...</React.Fragment> — the shorthand <> can't take a key, so this is the one place you need the explicit Fragment.

Conditional rendering — the toolkit. JSX only allows expressions, so conditionals are expressed as expressions. Ternary for if/else: {isAdmin ? <Admin /> : <Guest />}. && for render-if-only (no else): {error && <Alert msg={error} />}. Early return for whole-component branches (loading, error, empty) before the main JSX. Assign to a variable before return when the logic is too gnarly for inline. All four are common and idiomatic.

The && falsy-number pitfall. {count && <Badge n={count} />} renders the literal 0 when count is 0, because 0 is falsy and short-circuits to 0 — which React renders as text. Always guard with a boolean condition: {count > 0 && <Badge n={count} />}, or use a ternary {count ? <Badge n={count} /> : null}. The same trap applies to empty strings.

Empty, loading, and error states — render them deliberately. Real lists are rarely just 'the data.' Handle the states explicitly, usually with early returns: if (loading) return <Spinner />; then if (error) return <Error />; then if (items.length === 0) return <Empty />; and finally the mapped list. Forgetting the empty state (rendering a blank <ul>) is a classic UX bug.

Composition of the patterns. A typical list component layers them: guard the loading/error/empty states first, then .map() the data into keyed elements, and inside each item use ternaries and && to vary its display (a badge if approved, a button if actionable). Keeping keys stable is the single most important rule because it preserves item state through all this.

Keys are for React, not for you. Keys never appear in the DOM and you can't read them back inside the component (props.key is undefined) — if you need the id inside, pass it as a separate prop too. Also, keys only need to be unique among siblings, not globally, so two different lists can both use key={item.id} even if ids overlap across lists.

Performance note. Rendering huge lists (thousands of rows) even with correct keys can be slow because React still builds and diffs every element. For very large lists, use windowing/virtualization (e.g. react-window) to render only the visible rows. Stable keys remain essential — they let the virtualized library recycle rows correctly.

The mental model (memorise this). Map arrays to elements and give each a stable, unique key (a real id, never the index for dynamic lists) — keys give items identity so reconciliation preserves their state and DOM. Do conditionals with ternary (if/else), && (if-only, but guard 0), and early returns for loading/error/empty. Layer them: guard states, then map to keyed elements, then vary each item internally.

Backend Analogy

A key is a stable primary key / entity id, and reconciliation is a diff/merge against the previous result set: with a real id, React (like an upsert keyed on the PK) knows exactly which rows to update, insert, delete, or leave alone, so per-row state survives. Keying by array index is like keying a cache or a database join on row-ordinal instead of the primary key — reorder the result set and every downstream association silently points at the wrong record, the classic source of 'the data looks shuffled' bugs. Conditional rendering (ternary/&&/early return) is the same guard-clause discipline you use in a handler: validate and short-circuit the loading/error/empty cases before the happy path. Virtualizing a huge list is pagination/streaming — you never materialize the whole result set in the viewport, you render the visible window and recycle rows keyed by id.

Key Insights
  • Render lists by mapping an array to elements; JSX accepts an array of elements as children.
  • Every list item needs a key that is stable and unique among its siblings.
  • Keys give items identity so reconciliation can match them across renders, preserving their state and DOM.
  • Never use the array index as a key for dynamic lists — reorder/insert/remove makes the index point at a different item, causing state to attach to the wrong row.
  • Put the key on the outermost mapped element; use <React.Fragment key=...> when an item needs multiple siblings (the <> shorthand can't take a key).
  • Conditionals: ternary for if/else, && for if-only, early return for whole-component branches, or a variable before return for complex logic.
  • Guard the && number pitfall: {count > 0 && ...} — a bare {count && ...} renders a stray 0 when count is 0.
  • Handle loading, error, and especially empty states explicitly — a blank list is a common UX bug.
  • Keys are for React only: they don't appear in the DOM and can't be read via props.key; they need only be unique per sibling list.
  • For very large lists, use windowing/virtualization; stable keys still matter so rows recycle correctly.

Worked Code

List with keys plus loading/error/empty guards
TSX
type Expense = { id: number; amount: number; category: string; approved: boolean };

function ExpenseList({ items, loading, error }:
  { items: Expense[]; loading: boolean; error?: string }) {

  // Guard states first, with early returns.
  if (loading) return <p>Loading…</p>;
  if (error)   return <p role="alert">{error}</p>;
  if (items.length === 0) return <p className="muted">No expenses yet.</p>;

  return (
    <ul>
      {items.map(expense => (
        // key: stable & unique among siblings — use the id, NEVER the index
        <li key={expense.id}>
{expense.amount} · {expense.category}
          {/* && for if-only; ternary inside the item to vary display */}
          {expense.approved
            ? <span> ✅ approved</span>
            : <button> Approve</button>}
        </li>
      ))}
    </ul>
  );
}
Why index keys break: state attaches to the wrong row
TSX
import { useState } from 'react';

function TodoList() {
  const [todos, setTodos] = useState(['Buy milk', 'Walk dog', 'Pay bills']);

  const removeFirst = () => setTodos(prev => prev.slice(1));

  return (
    <>
      <button onClick={removeFirst}>Remove first</button>
      <ul>
        {todos.map((todo, index) => (
          // ❌ key={index}: after removing the first item, every remaining
          //    item shifts index, so the <input>'s typed text (its state)
          //    stays with the OLD position and appears on the WRONG todo.
          // ✅ Fix: give each todo a real stable id and key by that.
          <li key={index}>
            <input defaultValue={todo} /> {/* uncontrolled state = the bug */}
          </li>
        ))}
      </ul>
    </>
  );
}
Conditional rendering patterns and the 0 pitfall
TSX
function StatusBar({ count, isAdmin, error }:
  { count: number; isAdmin: boolean; error?: string }) {

  // Compute complex JSX into a variable before return.
  const role = isAdmin ? <b>Admin</b> : <span>Member</span>;

  return (
    <div>
      {/* ternary for if/else */}
      {role}

      {/* && for if-only (no else) */}
      {error && <p role="alert">{error}</p>}

      {/* ❌ {count && ...} renders "0" when count === 0 */}
      {/* ✅ guard with a boolean so nothing renders at 0 */}
      {count > 0 && <span>{count} new items</span>}

      {/* ternary returning null is another clean if-only */}
      {count > 0 ? <span> (has items)</span> : null}
    </div>
  );
}
Fragments in a list (multiple siblings per item, keyed)
TSX
import React from 'react';

type Row = { id: number; term: string; definition: string };

function Glossary({ rows }: { rows: Row[] }) {
  return (
    <dl>
      {rows.map(row => (
        // Each item needs TWO siblings (<dt>+<dd>) with no wrapper element.
        // The <> shorthand can't take a key, so use the explicit Fragment.
        <React.Fragment key={row.id}>
          <dt>{row.term}</dt>
          <dd>{row.definition}</dd>
        </React.Fragment>
      ))}
    </dl>
  );
}

Try It Live

Edit the code and press Run — it executes safely in a sandboxed iframe. Use the Console tab for log output.

Index keys vs id keys — see state stick to the wrong row

Interview-Ready Q&A

Keys give each list item a stable identity so React can match elements between renders during reconciliation, preserving their state and DOM nodes when the list changes. The array index is positional, not identity: if items are inserted, removed, reordered, or filtered, the same index points to a different item, so React reuses the wrong instance and DOM/state — causing bugs like inputs or checkboxes showing on the wrong row. Use a real domain id; index keys are acceptable only for a static, never-reordered list.

Things to Remember
  • 1Render lists with .map(); every item needs a stable, unique-among-siblings key.
  • 2Use a real id for keys — NEVER the array index for lists that reorder, insert, or remove.
  • 3Keys give identity: they preserve item state and DOM across renders via reconciliation.
  • 4Put the key on the outermost mapped element; use <React.Fragment key=...> for multi-sibling items.
  • 5Ternary for if/else, && for if-only, early return for loading/error/empty branches.
  • 6Guard the && zero pitfall: {count > 0 && ...}, not {count && ...}.
  • 7Always handle the empty state explicitly — a blank list is a UX bug.
  • 8Compute complex JSX into a variable before return when inline gets unreadable.
  • 9Keys aren't readable in the component (props.key is undefined) and are only unique per sibling list.
  • 10For huge lists use virtualization; stable keys still matter for row recycling.

References & Further Reading