Skip to main content

πŸ”€ Conditional Rendering Patterns

A component rarely shows the same thing every time. Logged in or out? Loading, loaded, or errored? Cart empty or full? React has no <if> tag β€” instead you use plain JavaScript to decide what JSX to return. This lesson covers the four patterns that handle every case: the && operator, the ternary, early returns, and element variables.

Week 4 · Day 4 (Thursday: Lists and Conditional Rendering) · Lecture 3

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Show or hide an element with the && operator β€” and avoid its falsy-0 trap
  • Choose between two branches inline with the ternary operator
  • Use early returns to peel off loading, error, and empty states cleanly
  • Assign JSX to an element variable to keep complex return markup readable
  • Render nothing safely by returning null
  • Pick the right pattern for a given situation and avoid deeply nested ternaries

Estimated Time: 55 minutes

Practice: Build a data view that switches between loading, error, empty, and content states.

In This Lesson

Why Conditional Rendering?

Your UI is a function of state. When state says "still loading," you show a spinner; when it says "here's the data," you show the list; when it says "something broke," you show an error. Because JSX is just JavaScript expressions, you steer it with the same tools you already know: &&, the ternary ? :, if statements, and variables.

graph TD A[Component state] --> B{Which case?} B -->|loading| C[Show spinner] B -->|error| D[Show error message] B -->|empty| E[Show empty state] B -->|has data| F[Show content]

The trick is knowing which of the four patterns fits. A one-sided "show this only if" wants &&. A two-sided "this or that" wants a ternary. A whole component that branches into several full-screen states wants early returns. And a chunk of markup you want to compute before the return wants an element variable. Let's take them one at a time.

Pattern 1: The && Operator

Use condition && <element> when you want to render something only when a condition is true, and render nothing otherwise. It works because of how JavaScript's && evaluates: if the left side is truthy, the expression becomes the right side; if the left side is falsy, the expression becomes the left side (which React ignores when it is false, null, or undefined).

function Inbox({ unreadCount, isAdmin }) {
    return (
        <header>
            <h1>Inbox</h1>

            {/* Renders the badge only when there are unread messages */}
            {unreadCount > 0 && (
                <span className="badge">{unreadCount} new</span>
            )}

            {/* Renders the admin link only for admins */}
            {isAdmin && <a href="/admin">Admin</a>}
        </header>
    );
}

⚠️ The falsy-0 trap β€” the classic beginner bug

If the left side is a number that can be 0, you will render a literal 0 on the page. React renders false/null/undefined as nothing β€” but 0 is a real value it happily displays.

// ❌ When items.length is 0, this puts a stray "0" on screen.
{items.length && <ItemList items={items} />}

// βœ… Force a real boolean on the left side.
{items.length > 0 && <ItemList items={items} />}

// βœ… Or coerce with a double-bang.
{!!items.length && <ItemList items={items} />}

Rule of thumb: never put a bare number on the left of && in JSX. Compare it (> 0) so the left operand is always true or false.

You can stack several independent && lines for a component that layers optional pieces:

function Profile({ user, isLoading, error }) {
    return (
        <div>
            {isLoading && <Spinner />}
            {error && <ErrorBanner message={error} />}
            {user && !isLoading && !error && (
                <section>
                    <h2>{user.name}</h2>
                    <p>{user.bio}</p>
                </section>
            )}
        </div>
    );
}

That last block hints at a smell: when you find yourself writing user && !isLoading && !error, the states are really mutually exclusive, and early returns (Pattern 3) will read far better.

Pattern 2: The Ternary

Use condition ? <a> : <b> when you must choose between two alternatives β€” an either/or. Unlike &&, the ternary always renders one branch or the other, so it is the tool for "this or that," including toggling a className.

function ConnectionStatus({ isOnline }) {
    return (
        <span className={isOnline ? 'status online' : 'status offline'}>
            {isOnline ? 'Online' : 'Offline'}
        </span>
    );
}

function AuthButton({ user, onLogin, onLogout }) {
    return user
        ? <button onClick={onLogout}>Log out</button>
        : <button onClick={onLogin}>Log in</button>;
}

Ternaries shine for small inline choices. They become unreadable when nested. This works but nobody enjoys maintaining it:

// ❌ Nested ternaries β€” hard to read, easy to break.
function Dashboard({ user }) {
    return (
        <div>
            {user ? (
                user.isAdmin ? <AdminPanel /> : <UserPanel />
            ) : (
                <GuestPanel />
            )}
        </div>
    );
}

βœ… Flatten nested ternaries into early returns

function Dashboard({ user }) {
    if (!user) return <GuestPanel />;
    if (user.isAdmin) return <AdminPanel />;
    return <UserPanel />;
}

Same logic, read top to bottom like a checklist. Reserve ternaries for a single, shallow either/or; reach for early returns the moment a second condition appears.

Pattern 3: Early Returns

When a whole component swaps between several distinct, full states, put if statements before the main return and return early for each special case. Each guard handles one case and exits, so by the time execution reaches the bottom you are on the guaranteed "happy path." This is the cleanest pattern for the loading/error/empty/content quartet you will write constantly.

import { useState, useEffect } from 'react';

function OrdersView() {
    const [orders, setOrders] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError]     = useState(null);

    useEffect(() => {
        fetchOrders()
            .then(setOrders)
            .catch((err) => setError(err.message))
            .finally(() => setLoading(false));
    }, []);

    // Each guard peels off one state and returns.
    if (loading) return <Spinner label="Loading orders…" />;
    if (error)   return <ErrorBanner message={error} />;
    if (orders.length === 0) return <EmptyState label="No orders yet" />;

    // Happy path: we know orders is a non-empty array.
    return (
        <ul>
            {orders.map((order) => (
                <li key={order.id}>{order.summary}</li>
            ))}
        </ul>
    );
}
Early returns acting as sequential guards that each peel off one state before the happy path render starts loading? yes β†’ Spinner error? yes β†’ ErrorBanner empty? yes β†’ EmptyState happy path: render the list
Early returns read like a checklist: each special state exits early, leaving a clean happy path at the bottom.

πŸ’‘ Guards must come after all Hooks

The Rules of Hooks require every useState/useEffect to run on every render, in the same order. So call your Hooks first, at the top of the component, and only then do your early returns. Never put a return above a Hook, or React will complain about a changing hook order.

Pattern 4: Element Variables

Sometimes you want to compute a piece of markup with ordinary if logic, then drop it into a larger layout that is shared across all cases. Assign the JSX to a variable before the return, then reference {variable} inside the markup. This keeps the surrounding structure (a header, a footer, a wrapper) written once, with only the middle varying.

function MessagePanel({ status, data }) {
    // Build the body with plain if/else β€” no JSX gymnastics.
    let body;
    if (status === 'loading') {
        body = <Spinner />;
    } else if (status === 'error') {
        body = <p role="alert">Something went wrong.</p>;
    } else {
        body = <DataTable rows={data} />;
    }

    // The wrapper is written once; only `body` changes.
    return (
        <section className="panel">
            <h2>Activity</h2>
            {body}
            <footer>Last updated just now</footer>
        </section>
    );
}

A close cousin is the object-map (lookup) pattern, which replaces a long switch with a plain object keyed by the state value:

function StatusIcon({ status }) {
    const icons = {
        success: <CheckIcon />,
        warning: <WarnIcon />,
        error:   <ErrorIcon />,
    };
    // Fall back to a default for any unknown status.
    return icons[status] ?? <InfoIcon />;
}

πŸ“– When to reach for element variables

Use an element variable when the branching logic is more than a line or two and the result sits inside shared surrounding markup. If the whole component just becomes a different thing per state, early returns are simpler. If you're picking one of many by a key, the object-map is tidiest.

Rendering Nothing

A component is allowed to render nothing: return null and React renders an empty result (no DOM). This is how a component politely opts out.

function WarningBanner({ show, children }) {
    if (!show) return null; // render nothing at all
    return <div className="warning">{children}</div>;
}

⚠️ Returning null vs not mounting

Returning null still mounts the component and runs its Hooks β€” it just outputs nothing. That is fine and often desirable. But if the component is expensive, consider not rendering it at all from the parent ({show && <Expensive />}) so it never mounts in the first place. Do not return false or undefined expecting the same clean result in every context β€” null is the explicit, intentional choice.

Choosing a Pattern

A quick decision guide you can keep in your head:

SituationPatternExample
Show something only if true (one-sided)&&{isAdmin && <AdminLink />}
Choose between exactly two optionsTernary{on ? 'On' : 'Off'}
Whole component has several full statesEarly returnsif (loading) return …
Compute markup, drop into shared layoutElement variablelet body = …; return <div>{body}</div>
Pick one of many by a keyObject mapicons[status]
Render nothingreturn nullif (!show) return null;

These are not rigid rules β€” they are the readable default for each shape of problem. The guiding principle is the same one you have met all week: keep the branching logic obvious, and keep JSX shallow.

Practice & Quiz

πŸ‹οΈ Exercise 1: Fix the zero trap

Goal: This cart badge prints a stray "0" when the cart is empty. Fix it so it renders nothing when there are no items, and the count otherwise.

function CartBadge({ items }) {
    return (
        <div className="cart">
            πŸ›’
            {items.length && <span className="count">{items.length}</span>}
        </div>
    );
}
πŸ’‘ Hint

The left operand of && is a bare number that can be 0. Turn it into a real boolean with a comparison.

βœ… Solution
function CartBadge({ items }) {
    return (
        <div className="cart">
            πŸ›’
            {items.length > 0 && (
                <span className="count">{items.length}</span>
            )}
        </div>
    );
}

πŸ‹οΈ Exercise 2: Four-state data view

Goal: Given loading, error, and users (an array), render a spinner, an error message, an empty state, or the list β€” using early returns.

βœ… Solution
function UserList({ loading, error, users }) {
    if (loading) return <p>Loading…</p>;
    if (error)   return <p role="alert">Error: {error}</p>;
    if (users.length === 0) return <p>No users found.</p>;

    return (
        <ul>
            {users.map((u) => (
                <li key={u.id}>{u.name}</li>
            ))}
        </ul>
    );
}

🎯 Quick Quiz

Question 1: Which pattern best fits "show this element only when the user is an admin"?

Question 2: Why does {count && <Badge />} sometimes render a literal 0?

Question 3: You have a component that must show loading, error, empty, and content states. What reads best?

Best Practices & Pitfalls

βœ… Do

  • Use && for one-sided show/hide, ternary for a single either/or
  • Prefer early returns for mutually exclusive full-component states
  • Put a comparison (> 0) on the left of && when the value could be 0
  • Extract complex branching into a helper function, element variable, or object map
  • Call all Hooks before any early return

❌ Don't

  • Nest ternaries more than one level deep
  • Put a bare number on the left of && in JSX
  • Return undefined when you mean "render nothing" β€” return null
  • Place a return above a useState/useEffect call

⚠️ Mount vs hide

// Unmounts when hidden β€” loses internal state each time.
{show && <Panel />}

// Stays mounted, just visually hidden β€” keeps state, costs memory.
<div style={{ display: show ? 'block' : 'none' }}>
    <Panel />
</div>

Conditional rendering unmounts the hidden branch, which resets its state and stops its effects. If you need a frequently toggled panel to remember scroll position or input, hide it with CSS instead of unmounting it.

Summary

πŸŽ‰ Key Takeaways

  • && renders one-sided "only if true" β€” but guard against the falsy-0 trap with a comparison
  • Ternary picks between exactly two branches; keep it shallow, never nested
  • Early returns handle several mutually exclusive states and read like a checklist
  • Element variables (and object maps) compute markup with plain if logic for a shared layout
  • Return null to render nothing; remember Hooks run before any early return

πŸ“š Additional Resources

πŸš€ What's Next?

You can now render the right amount of UI (lists) and the right UI for the state (conditionals). Next you'll learn how React encourages you to build UI: Composition vs inheritance β€” why React apps combine small components with children and props instead of extending classes, and how that keeps your component tree flexible.

πŸŽ‰ Four patterns, mastered!

Show, hide, branch, and swap states with confidence β€” and you'll never ship a stray 0 again.