Conditional Rendering

Showing and hiding UI, and the falsy values that render when you did not expect them to.

What you'll learn

  • The three ways to render conditionally, and when each reads best
  • Why && sometimes renders a stray 0
  • What React renders for null, undefined, and false
  • Why hiding with CSS is not the same as not rendering

There is no special syntax

Conditional rendering is just JavaScript. Braces in JSX take an expression, so you have three tools.

1. Ternary, for either/or

import { useState } from "react";

export default function App() {
  const [isSignedIn, setIsSignedIn] = useState(false);

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <p>{isSignedIn ? "Welcome back" : "Please sign in"}</p>

      <button onClick={() => setIsSignedIn(!isSignedIn)}>
        {isSignedIn ? "Sign out" : "Sign in"}
      </button>
    </div>
  );
}

Ternaries nest badly. Two levels deep is usually the signal to pull the branches into variables or separate components.

2. &&, for show-or-nothing

When there is no "else", && is shorter. It works because a && b evaluates to b when a is truthy, and to a when it is not:

import { useState } from "react";

export default function App() {
  const [items, setItems] = useState(["Book"]);

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <button onClick={() => setItems([...items, "Item " + (items.length + 1)])}>
        Add
      </button>
      <button onClick={() => setItems([])} style={{ marginLeft: 8 }}>
        Clear
      </button>

      {items.length > 0 && <p>{items.length} item(s) in the cart</p>}
      {items.length > 2 && <p style={{ color: "crimson" }}>That is a lot.</p>}
    </div>
  );
}

3. An early return, for whole-component branches

When the two cases share nothing, stop pretending they are one tree:

function Profile({ user }) {
  if (!user) {
    return <p>No user selected.</p>;
  }

  return <h1>{user.name}</h1>;
}

The && trap

This is the one that catches everyone. If the left side is a falsy value that React can render, React renders it.

0 is the usual culprit: items.length && <p>...</p> evaluates to 0 when the array is empty, and React prints 0 on the page.

export default function App() {
  const items = [];
  const name = "";

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <p>Broken — a stray zero appears below:</p>
      <div style={{ border: "1px solid #ddd", padding: 8 }}>
        {items.length && <span>has items</span>}
      </div>

      <p style={{ marginTop: 12 }}>Fixed with an explicit comparison:</p>
      <div style={{ border: "1px solid #ddd", padding: 8 }}>
        {items.length > 0 && <span>has items</span>}
        {name !== "" && <span>has a name</span>}
      </div>
    </div>
  );
}

The habit that avoids it: put a boolean on the left of &&, not a number or a string. items.length > 0, not items.length.

What React renders for nothing

null, undefined, false, and true all render nothing at all. That is why && works, and why returning null is the way to render nothing:

function Nothing() {
  return null;
}

export default function App() {
  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <p>Between the rules, every one of these renders nothing:</p>
      <hr />
      {null}
      {undefined}
      {false}
      {true}
      <Nothing />
      <hr />
      <p>But 0 and "" are values, so they do render:</p>
      <div style={{ border: "1px solid #ddd", padding: 8 }}>[{0}]</div>
    </div>
  );
}

Not rendering is not the same as hiding

{show && <Panel />} removes Panel from the tree. It is unmounted: its state is discarded, and it starts fresh when it comes back.

<Panel hidden={!show} /> keeps it mounted and invisible. Its state survives, and its effects keep running.

Neither is wrong — but they differ, and the difference matters once components hold state. If a form resets every time it is hidden, this is why.

Common mistakes

  • items.length &&. Renders 0 on an empty array. Compare explicitly.
  • Nesting ternaries. Extract a variable or a component instead.
  • Returning undefined. A component must return something; use null.
  • if inside braces. It is a statement; use a ternary or return early.

Exercise

Two bugs, both visible in the preview. The greeting reads "Hello," with nothing after it when there is no name, and the cart line shows a stray 0 while the cart is empty.

Fix both: show a signed-out message when there is no name, and render the count only when there is something in the cart.

import { useState } from "react";

export default function App() {
  const [cart, setCart] = useState([]);
  const user = { name: "" };

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <p>Hello, {user.name}</p>

      <button onClick={() => setCart([...cart, "item"])}>Add to cart</button>
      <div style={{ border: "1px solid #ddd", padding: 8, marginTop: 8 }}>
        {cart.length && <span>{cart.length} in cart</span>}
      </div>
    </div>
  );
}