Skip to content

Conditional rendering

A logged-in visitor sees a dashboard, a logged-out visitor sees a login form, and a failed request needs an error message to show up somewhere on the page. A boolean like isLoggedIn usually lives in state, and what changes is which piece of UI gets rendered for its current value. JSX is JavaScript, so deciding what to render works the same way any other decision in your code works: an expression that picks a value, evaluated right there in the return statement.

Choosing between two elements with a ternary

When there are exactly two things a piece of UI could be, the ternary operator picks between them:

jsx
return (
  <div>
    {isLoggedIn ? <Dashboard /> : <Login />}
    {hasError && <p>Something went wrong</p>}
  </div>
)

isLoggedIn ? <Dashboard /> : <Login /> reads the same as any other ternary: if isLoggedIn is true, this expression evaluates to <Dashboard />, otherwise it evaluates to <Login />. Whatever it evaluates to gets rendered in that spot in the JSX. Curly braces are what let you drop a JavaScript expression into the middle of markup, and a ternary is one expression like any other.

Showing or hiding one element with &&

The second line handles a different shape of decision: show something, or show nothing. hasError && <p>Something went wrong</p> uses the && operator the way it works everywhere else in JavaScript. If hasError is false, && short-circuits and the whole expression evaluates to false, without ever reaching the JSX on the right. If hasError is true, the expression evaluates to the <p> element.

The reason this renders correctly comes down to what React does with the result. React skips rendering anything for false, null, and undefined, so when hasError is false, nothing shows up on the page at all.

The falsy value gotcha

&& doesn't only produce true or false. Like any JavaScript expression using &&, it evaluates to whichever side it lands on, and that side can be any value, not necessarily a boolean. Most of the time that's harmless, but it turns into a bug when the left side is a number:

jsx
{count && <Badge />}
// count = 0 → renders "0" on the page

If count is 0, this expression evaluates to 0. React skips rendering for false, null, and undefined, but 0 is a real, renderable value, so React puts it on the page. The badge doesn't show, but a stray 0 does, sitting right where you expected nothing.

The fix is to make sure the left side of && is always an actual boolean:

jsx
{count > 0 && <Badge />}
// count = 0 → renders nothing

count > 0 always evaluates to true or false, so the expression either renders the badge or renders nothing, with no 0 left behind.

Rendering nothing at all

Sometimes a component has nothing to show, and the clearest way to say that is to return early:

jsx
function Banner({ message }) {
  if (!message) {
    return null
  }

  return <p className="banner">{message}</p>
}

When message is empty, the function returns null before it ever builds the rest of the JSX. That keeps the main return focused on the case where there's actually something to render, instead of wrapping the whole thing in one more condition.

Which tool to reach for comes down to what the condition is choosing between. Two elements: use a ternary. One element or nothing: use &&. A component with nothing to render at all: return early with if (!x) return null before the main return, rather than wrapping the whole JSX tree in one more condition. Mixing them, a ternary where && would do or an early return buried inside a ternary, is usually a sign to switch to the more direct tool.

The falsy-gotcha lesson from {count && <Badge />} is not limited to 0. NaN renders the same way, so {total / count && <Badge />} can print NaN on the page if count is 0. An empty string "" is the quieter version of the same bug: {name && <Greeting name={name} />} renders nothing visible when name is "", because an empty string on the page is invisible, but it is still a stray text node sitting in the DOM. The fix is the same one used for 0: force the left side to an explicit boolean, with a comparison like count > 0 or a Boolean(...) call, instead of trusting the raw value to be falsy in the way you expect.

Conditions get harder to read once more than one is in play. A ternary nested inside another ternary in JSX is the first sign a condition has outgrown inline logic:

jsx
{status === 'loading' ? <Spinner /> : status === 'error' ? <ErrorMessage /> : <Content />}

Two options for cleaning this up. Pull the decision out into a variable above the return, so the JSX only has to embed the result:

jsx
const view =
  status === 'loading' ? <Spinner /> :
  status === 'error' ? <ErrorMessage /> :
  <Content />

return <div>{view}</div>

Or extract the logic into a small helper function that returns the right element, which reads better once there are more than two or three branches. Either way, the goal is the same: keep the JSX itself free of decisions and let it embed values that were already decided above it.

Returning null from a component renders nothing: no DOM node gets created for it, not even an empty one. It's a legitimate return value for a component, and React treats a null return the same way it treats an empty fragment.

There's a second thing worth knowing once you start swapping components in and out of the same spot. React reconciles the tree by walking it position by position, and at each position it compares the type of element that's there now against the type that was there on the last render.

jsx
{isEditing ? <EditForm /> : <ViewForm />}

When isEditing flips, EditForm and ViewForm are different component types occupying the same position, so React unmounts the old one and mounts the new one from scratch. Any state EditForm was holding, an input value the user was typing, for example, is gone the moment ViewForm takes its place. This is different from rendering the same component type with different props at that position: same type at the same position means React updates the existing instance and its state survives. The type is what determines whether React sees "the same thing, updated" or "a new thing entirely."

JunoDeciding what to show is ordinary JavaScript A ternary picks between two elements, && shows one element or shows nothing, and returning null from a component is how you say "there's nothing to render here." Watch out for {count && <Badge />} when count can be 0, since 0 is a value React will actually print. Writing {count > 0 && <Badge />} instead keeps the left side a true boolean.
JunoDeciding what to show is ordinary JavaScript Reach for a ternary when you're choosing between two elements, and && when you're choosing between one element and nothing. The gotcha to remember with && is that it evaluates to whichever side it lands on, so a falsy number like 0 gets rendered as itself instead of disappearing. Guard against it by making the left side an explicit boolean, like count > 0, and use an early return null when a component has nothing to show.
JunoDeciding what to show is ordinary JavaScript Every conditional rendering trick here is ordinary JavaScript evaluated inside JSX, and the only React-specific part is that false, null, and undefined render as nothing while every other value, including 0, renders as itself. That's what makes {count && <Badge />} a trap and {count > 0 && <Badge />} the fix. The part worth carrying forward is reconciliation by position and type: swap in a different component type at the same spot and its state is gone, because React sees a new element, not an update to the old one.

Next up: Forms, where you'll use state to handle input fields.