Lesson 8 of 12

Lists, Keys & Conditional Rendering

You will learn how to render and show content conditionally, and how to avoid two common : missing keys and a stray 0 appearing on the page.

JSX
function TodoList({ todos, filter }) {
  const visible = todos.filter(t =>
    filter === 'all' ? true : filter === 'done' ? t.done : !t.done
  )

  return (
    <ul>
      {visible.map(todo => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </ul>
  )
}

// The key tells React which item is which between renders.
// Use a STABLE id from your data. The array index works only if the list
// is never reordered, filtered, or inserted into — which is rarely true.

// Wrong: state and DOM follow the position, not the item
{todos.map((todo, i) => <TodoItem key={i} todo={todo} />)}

// Also wrong: a new key every render remounts the whole row every time
{todos.map(todo => <TodoItem key={Math.random()} todo={todo} />)}

// Right
{todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
JSX
function Dashboard({ user, notifications, error, isLoading }) {
  // Early returns keep the happy path flat and readable
  if (isLoading) return <Spinner />
  if (error) return <ErrorBanner message={error.message} />
  if (!user) return <LoginPrompt />

  return (
    <div>
      {/* Ternary for either/or */}
      {user.isAdmin ? <AdminPanel /> : <UserPanel />}

      {/* && for "render this or nothing" */}
      {notifications.length > 0 && <Badge count={notifications.length} />}

      {/* CAREFUL: 0 is falsy but React RENDERS it. This prints a bare "0". */}
      {notifications.length && <Badge count={notifications.length} />}

      {/* Fixes: force a boolean, or use a ternary */}
      {notifications.length > 0 && <Badge count={notifications.length} />}
      {notifications.length ? <Badge count={notifications.length} /> : null}

      {/* Nullish values render nothing: null, undefined, false, "" */}
      {user.bio ?? <em>No bio yet</em>}

      {/* Fragments group without adding a DOM node */}
      <>
        <h2>{user.name}</h2>
        <p>{user.email}</p>
      </>
    </div>
  )
}
JSX
// Empty, loading and error states are part of the component, not an afterthought.
function SearchResults({ query, results, status }) {
  if (status === 'idle')    return <p>Type to search.</p>
  if (status === 'loading') return <SkeletonList count={5} />
  if (status === 'error')   return <p role="alert">Something went wrong.</p>
  if (results.length === 0) return <p>No results for "{query}".</p>

  return (
    <ol>
      {results.map(r => (
        <li key={r.id}>
          <a href={r.url}>{r.title}</a>
          {r.isNew && <span className="badge">New</span>}
        </li>
      ))}
    </ol>
  )
}

// Rendering a lookup table instead of a switch
const ICONS = { success: '✓', warning: '!', error: '×' }

function Alert({ type = 'success', children }) {
  return (
    <div className={`alert alert-${type}`}>
      <span aria-hidden="true">{ICONS[type]}</span>
      {children}
    </div>
  )
}

// Grouped lists: build the groups first, render second
function GroupedTodos({ todos }) {
  const groups = Object.groupBy(todos, t => t.category)
  return Object.entries(groups).map(([category, items]) => (
    <section key={category}>
      <h3>{category}</h3>
      <ul>{items.map(t => <li key={t.id}>{t.title}</li>)}</ul>
    </section>
  ))
}
Finished reading?Mark it done to track your progress.

Tip: use the ← and → keys to move between lessons.