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.
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} />)}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>
)
}// 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.