Conditional rendering

Show things sometimes, and cover every state a screen can be in.

05of 20
≈ 10Min read
7Parts

What renders and what does not

the values React puts on the page
{"text"}        // the text
{42}            // the number
{0}             // ZERO — this one catches everyone
{elements}      // elements and arrays of them

{null}          // nothing
{undefined}     // nothing
{false}         // nothing
{true}          // nothing
{[]}            // nothing
0 is not falsy enough

{items.length && <List />} renders 0 when the array is empty. {items.length > 0 && <List />} renders nothing. Always make the left side a boolean.

The three forms

Nested ternaries past one level become unreadable fast. Assigning to a variable above the return costs three lines and stays readable at five outcomes.

and when each reads best
// show or nothing
{error && <p className="error">{error}</p>}

// one or the other
{isOpen ? <Panel /> : <Placeholder />}

// several outcomes: decide above the return
let body;
if (status === "loading") body = <Spinner />;
else if (status === "error") body = <Error message={message} />;
else if (items.length === 0) body = <Empty />;
else body = <List items={items} />;

return <section>{body}</section>;

A lookup for many cases

when the cases are a fixed set
const VIEWS = {
  loading: <Spinner />,
  empty: <Empty />,
  error: <ErrorPanel />,
};

return VIEWS[status] ?? <List items={items} />;

Every screen has four states

Any interface that loads something can be in four states, and the one that gets forgotten is the third:

  • Loading — the request is in flight.
  • Ready — there is data, show it.
  • Empty — the request succeeded and there is nothing. This is not an error and must not look like one.
  • Error — the request failed, with a message and a way to try again.
the shape worth copying
function Doors({ status, doors, error, onRetry }) {
  if (status === "loading") return <p>Loading…</p>;
  if (status === "error") {
    return (
      <div role="alert">
        <p>{error}</p>
        <button onClick={onRetry}>Try again</button>
      </div>
    );
  }
  if (doors.length === 0) return <p>No doors yet. Add the first one.</p>;

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

Early returns keep each state on its own line of reasoning. An empty state that says what to do next is worth more than one that says "No results".

Hiding with CSS, or not rendering

If the panel holds a half-typed form you do not want to lose, keep it mounted and hide it. If it is expensive and starts fresh each time, do not render it.

they are different
// not rendered: no DOM, no cost, and state inside it is LOST
{isOpen && <Panel />}

// rendered and hidden: keeps its DOM and its state, costs the render
<Panel hidden={!isOpen} />
<div style={{ display: isOpen ? "block" : "none" }}><Panel /></div>

Guarding against missing data

the first render often has nothing
function Profile({ user }) {
  if (!user) return null;                  // the honest guard
  return <p>{user.name}</p>;
}

// or handle it where the data lives
{user ? <Profile user={user} /> : <Skeleton />}

// and optional chaining for one level
<p>{user?.address?.city ?? "Unknown"}</p>

Returning nothing

return null is a valid, ordinary thing for a component to do. It is not an error and it is not a placeholder — it means "there is nothing to show here", which is exactly what a conditional row, an empty toast or a hidden tooltip needs.

Practice