Skip to content

Conditional Rendering and Lists

intermediate12 min read

Conditional Rendering Is Just JavaScript

There is no v-if or ngIf in React. Conditional rendering uses plain JavaScript expressions inside JSX. This is a direct consequence of JSX being function calls, not templates.

function Dashboard({ user }) {
  // Approach 1: Ternary (inline)
  return (
    <div>
      {user.isAdmin ? <AdminPanel /> : <UserPanel />}
    </div>
  );

  // Approach 2: Logical AND (show or nothing)
  return (
    <div>
      {user.isAdmin && <AdminPanel />}
    </div>
  );

  // Approach 3: Early return (entire component)
  if (!user) return <LoginPrompt />;
  return <Dashboard user={user} />;
}
Mental Model

Think of conditional rendering as a switch on a railroad track. The train (render cycle) follows one path or another based on the switch position (condition). The path not taken does not exist — React does not create elements for the false branch. When the switch flips, React tears down the old path and builds the new one. If you use a key prop, you can force React to rebuild even when the switch stays on the same track.

The && Trap: Rendering 0

The most common conditional rendering bug:

function Notifications({ count }) {
  // BUG: When count is 0, this renders "0" on screen
  return <div>{count && <Badge count={count} />}</div>;

  // 0 is falsy, so count && <Badge /> evaluates to 0.
  // React renders the number 0 as text.
}

JavaScript's && operator returns the first falsy value, not false. When count is 0, the expression evaluates to 0 — and React renders 0 as a text node.

// Fix 1: Explicit boolean comparison
{count > 0 && <Badge count={count} />}

// Fix 2: Double negation
{!!count && <Badge count={count} />}

// Fix 3: Ternary
{count ? <Badge count={count} /> : null}
Common Trap

This bug also affects empty strings and NaN. {'' && <Component />} renders an empty string (invisible but present in the DOM). {NaN && <Component />} renders the text "NaN". Always use explicit boolean checks: {items.length > 0 && <List />}, not {items.length && <List />}.

Lists and the Key Prop

When rendering arrays, React needs a way to track which items changed, were added, or were removed. The key prop is that tracking mechanism:

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

How Keys Drive Reconciliation

When a list re-renders, React compares old and new element arrays by key:

// Previous render:
[
  { key: 'a', type: 'li', props: { children: 'Apple' } },
  { key: 'b', type: 'li', props: { children: 'Banana' } },
  { key: 'c', type: 'li', props: { children: 'Cherry' } },
]

// Next render (item 'b' removed):
[
  { key: 'a', type: 'li', props: { children: 'Apple' } },
  { key: 'c', type: 'li', props: { children: 'Cherry' } },
]

// React's reconciliation:
// - key 'a': exists in both → update props if changed
// - key 'b': exists in old, missing in new → unmount
// - key 'c': exists in both → update props if changed

Without keys, React falls back to comparing by position. Insert an item at the beginning and every element after it gets updated or remounted — even if nothing changed about them.

Why Index as Key Is Dangerous

// DANGEROUS — using index as key:
{items.map((item, index) => (
  <li key={index}>{item.name}</li>
))}

The problem becomes visible when items are reordered, inserted, or deleted:

// Initial state (index as key):
// key=0: Apple    key=1: Banana    key=2: Cherry

// User deletes "Apple" (index 0):
// key=0: Banana   key=1: Cherry

// React's reconciliation sees:
// - key 0: was "Apple", now "Banana" → UPDATE props (reuse DOM node)
// - key 1: was "Banana", now "Cherry" → UPDATE props (reuse DOM node)
// - key 2: was "Cherry", now missing → UNMOUNT

// Result: React reuses the DOM nodes of Apple and Banana,
// just changing their text content. This BREAKS if those
// nodes have internal state (input values, focus, animations).
The index-key data corruption scenario

Consider a list of controlled inputs:

function EditableList({ items }) {
  return items.map((item, index) => (
    <input key={index} defaultValue={item.name} />
  ));
}

If you delete the first item, the input that had "Apple" (key=0) now gets "Banana" as its item. But the DOM input element still contains the user's typed text for "Apple" because React reused the node (same key). The displayed value does not match the data. This is a data corruption bug that is nearly impossible to diagnose without understanding keys.

Use stable, unique IDs: key={item.id}. If your data has no IDs, generate them when the data is created — not during render.

When Index Keys Are Safe

Index keys are acceptable only when ALL three conditions are true:

  1. The list is static (never reordered, filtered, or sorted)
  2. Items have no internal state (no inputs, no animations, no focus)
  3. Items are never inserted or deleted from the middle

In practice, these conditions rarely hold in production. Default to stable IDs.

Production Scenario: Conditional Loading States

function SearchResults({ query }) {
  const [results, setResults] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // ... fetch logic

  // Ordered conditions: error > loading > empty > results
  if (error) {
    return <ErrorState message={error.message} onRetry={retry} />;
  }

  if (loading) {
    return <SkeletonLoader count={5} />;
  }

  if (!results || results.length === 0) {
    return <EmptyState query={query} />;
  }

  return (
    <ul>
      {results.map(result => (
        <SearchResultCard key={result.id} result={result} />
      ))}
    </ul>
  );
}
Execution Trace
Render:
`todos = [(id:'a', text:'Buy'), (id:'b', text:'Cook')]`
Initial list render with stable keys
Elements:
`[(key:'a', children:'Buy'), (key:'b', children:'Cook')]`
React element array created
Insert:
`todos = [(id:'c', text:'Shop'), (id:'a', text:'Buy'), (id:'b', text:'Cook')]`
New item added at beginning
Reconcile:
key 'c': new → mount, key 'a': exists → reuse, key 'b': exists → reuse
Only ONE new DOM node created
Without keys:
index 0: was 'Buy' now 'Shop' → update, index 1: was 'Cook' now 'Buy' → update, index 2: new → mount
ALL existing nodes get updated text — wasteful and state-breaking
Common Mistakes
  • Wrong: Using && with values that can be 0, '', or NaN Right: Use explicit boolean checks: count > 0 && <Component />

  • Wrong: Using array index as key for dynamic lists Right: Use stable unique IDs: key={item.id}

  • Wrong: Generating keys during render: key={Math.random()} Right: Generate IDs when data is created, not during render

  • Wrong: Forgetting the key prop in mapped arrays Right: Always provide a key on the outermost element returned from .map()

Quiz
What does this render when count is 0: <div>`{count && <span>Items</span>}`</div>?
Quiz
A list of 100 items uses index as key. What happens when you delete item at index 0?
Quiz
Why does key={Math.random()} cause performance problems?
Key Rules
  1. 1Conditional rendering is plain JavaScript — ternary, &&, early return. No template directives.
  2. 2Watch for && with 0, '', NaN — they render as text. Use explicit boolean comparisons.
  3. 3Keys must be stable, unique, and predictable — use data IDs, never Math.random() or index for dynamic lists.
  4. 4Index keys are only safe for static lists with no state and no reordering.
  5. 5Keys tell React which items are the same across renders — wrong keys cause state corruption.

Challenge: Find the Key Bug

Key Prop Debugging