Lesson 8 of 20

Conditional Rendering

You Do Not Show and Hide — You Return Something Else

In plain JavaScript, making something conditional means reaching into the page and changing it: adding a hidden class, setting display: none, or removing a node. React reverses this. Your component is a function from data to UI, so a condition is simply a branch in that function. Different data, different returned UI.

This is a genuine mental shift and it is worth pausing on. There is no show() or hide() anywhere in React. If a message should only appear when a form has an error, you do not hide the message — you do not create it at all until error has a value. When the error clears, that piece of UI stops being returned, and React removes it from the page for you.

Because everything is ordinary JavaScript, you already know all the tools. There are only three shapes you need, and the rest of this lesson is about choosing between them and avoiding the two traps.

Example
// Plain JavaScript: reach into the page and change it
// document.getElementById('error').style.display = error ? 'block' : 'none';

// React: return different UI for different data
function Field({ label, value, error, onChange }) {
  return (
    <div>
      <label>{label}</label>
      <input value={value} onChange={onChange} />
      {error && <p className="error">{error}</p>}
    </div>
  );
}

// No error -> the <p> does not exist in the DOM at all.
// Error appears -> React inserts it. Error clears -> React removes it.
Notes
  • Returning null from a component is legal and means render nothing. It is the cleanest way for a component such as a banner or a toast to decide for itself that it has nothing to show.

The Three Shapes, and When to Use Each

The early return comes first, above the JSX, and is for whole-component branches: not logged in, still loading, no data. It keeps the main return free of nesting, because by the time you reach it you know the good case holds. When a component starts with three guard clauses and then renders normally, it is usually easy to read.

The ternary is for choosing between two alternatives inside the JSX: this text or that text, this button or that one. It is the only branching that works inline, since if is a statement and cannot go inside braces. Ternaries nested more than one level deep become unreadable quickly — when you get there, move the logic into a variable above the return.

The logical AND is for show this or show nothing. It works because a && b evaluates to a when a is falsy and to b otherwise, and React renders nothing for false, null and undefined. It is the most concise of the three, and it is also the one with the trap described in the next section.

Example
function Dashboard({ user, isLoading, notifications }) {
  // 1. Early returns — whole-component branches, handled first
  if (isLoading) return <Spinner />;
  if (!user) return <LoginPrompt />;

  // Complex logic belongs in a variable, not in a nested ternary
  let greeting;
  if (user.role === 'admin') greeting = 'Admin console';
  else if (user.role === 'teacher') greeting = 'Teacher dashboard';
  else greeting = 'Your dashboard';

  return (
    <div>
      <h1>{greeting}</h1>

      {/* 2. Ternary — one of two things */}
      <p>{user.isVerified ? 'Verified account' : 'Please verify your email'}</p>

      {/* 3. Logical AND — something or nothing */}
      {user.role === 'admin' && <button>Manage users</button>}

      {/* Ternary for an empty state */}
      {notifications.length > 0 ? (
        <NotificationList items={notifications} />
      ) : (
        <p>You are all caught up.</p>
      )}
    </div>
  );
}
  • Early return — the whole component renders something different
  • Ternary — pick between exactly two pieces of UI, inline
  • && — render something, or render nothing
  • A variable above the return — anything more complicated than the above
  • Never an if statement inside braces — it is a syntax error
Notes
  • If you catch yourself writing a ternary inside a ternary inside JSX, stop and extract a small component or a variable. Six months later, the nested version is unreadable even to the person who wrote it.

The && Trap: a Stray 0 on Your Page

This is React's most famous small bug, and it looks like a rendering fault rather than a JavaScript one. You write {cart.length && <CartSummary />}, expecting the summary to appear only when there is something in the cart. With an empty cart, a bare 0 appears on the page.

Trace it through carefully. cart.length is 0. In JavaScript, && returns the left operand when it is falsy — so the whole expression evaluates to the number 0, not to false. And React renders numbers. It renders false as nothing, but 0 is a perfectly good number, so it prints it. React is behaving exactly as documented; the expression simply did not produce what you assumed.

The fix is to give && a real boolean on its left. Write cart.length > 0 && .... The same problem hides in any falsy non-boolean: an empty string from an input, a 0 price, a count of zero. Get into the habit of putting a comparison there rather than a raw value, and the bug never happens.

There is a related, harmless-looking case: {user.nickname && <p>{user.nickname}</p>}. If nickname is an empty string, React renders an empty string, which shows nothing — so this one works by luck. Relying on luck is a poor habit; a comparison costs three characters.

Example
const cart = [];

// BROKEN — renders the character 0 on the page
{cart.length && <CartSummary items={cart} />}

// FIXED — the left side is now a real boolean
{cart.length > 0 && <CartSummary items={cart} />}

// Also fine
{Boolean(cart.length) && <CartSummary items={cart} />}
{!!cart.length && <CartSummary items={cart} />}

// Same trap with other falsy values
{discount && <p>You saved ₹{discount}</p>}          // renders 0 when discount is 0
{discount > 0 && <p>You saved ₹{discount}</p>}      // correct

// And in a ternary, which never has this problem
{cart.length > 0 ? <CartSummary items={cart} /> : <EmptyCart />}
Notes
  • If a mysterious 0 shows up in your UI, search your JSX for &&. It is that, essentially every time.

Conditional Attributes, Classes and Styles

Conditions are not only about whether an element exists. Just as often you want the same element with a different class, a disabled state, or different text. Since attribute values take any JavaScript expression, the same three shapes apply there too.

Class names are the common case. A template literal with a ternary handles it for one or two conditions and stays readable. Once you have three or more toggles, build the string in an array above the return, or use a tiny helper library such as clsx, which most real projects include for exactly this.

One detail about attributes is worth knowing: passing false, null or undefined to a boolean DOM attribute causes React to leave the attribute off entirely, which is what you want. So disabled={isSaving} is correct and complete — you never need a ternary that produces the string "disabled".

Example
function SaveButton({ isSaving, isDirty, variant }) {
  // Readable for a few conditions
  const className = `btn btn-${variant} ${isDirty ? 'btn-dirty' : ''}`;

  // Readable for many
  const classes = ['btn', `btn-${variant}`];
  if (isDirty) classes.push('btn-dirty');
  if (isSaving) classes.push('btn-busy');

  return (
    <button
      className={classes.join(' ')}
      disabled={isSaving || !isDirty}
      style={{ opacity: isSaving ? 0.6 : 1 }}
      aria-busy={isSaving}
    >
      {isSaving ? 'Saving…' : 'Save changes'}
    </button>
  );
}

// disabled={false} -> React omits the attribute completely
// disabled={true}  -> React writes disabled
  • Class strings — a template literal for one toggle, an array for several
  • Boolean attributes — disabled={cond}; false removes the attribute
  • Inline styles — an object with values chosen by a ternary
  • Text content — a ternary inside the element
  • Whole components — && or a ternary around the element

Guarding Data That Has Not Arrived Yet

The most frequent crash in a React app is Cannot read properties of undefined, and conditional rendering is the cure. It happens because a component renders before its data exists. Data fetched from a server arrives after the first render, so on that first pass user is null and user.name throws.

The reliable pattern is to handle every state a screen can be in, explicitly and in order: loading, error, empty, and finally the happy path. Beginners write the happy path and add the others after the bugs appear; writing all four upfront takes two extra minutes and removes an entire category of crash.

Optional chaining, user?.name, is a useful shortcut for one level of safety — it evaluates to undefined instead of throwing when user is missing, and React renders nothing for undefined. It is not a substitute for handling the loading state, though. Sprinkling question marks through a component to stop it crashing usually means the guard belongs higher up, in an early return.

Example
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [error, setError] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  // ... fetch omitted; the data-fetching lesson covers it

  // Handle every state, in order
  if (isLoading) return <p>Loading profile…</p>;
  if (error) return <p className="error">Could not load this profile.</p>;
  if (!user) return <p>No such user.</p>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>

      {/* Optional chaining for optional nested data */}
      <p>{user.address?.city ?? 'City not set'}</p>

      {/* An empty state, not a crash and not a blank space */}
      {user.courses.length > 0 ? (
        <ul>{user.courses.map(c => <li key={c.id}>{c.title}</li>)}</ul>
      ) : (
        <p>Not enrolled in any course yet.</p>
      )}
    </div>
  );
}
Notes
  • An empty state is part of the design, not an edge case. A screen that shows nothing at all when a list is empty looks broken to a user, who cannot tell the difference between no results and failed to load.

Not Rendering Is Not the Same as Hiding

One consequence of conditional rendering surprises people, and it is worth knowing before it costs you an afternoon. When a condition turns false and a component stops being rendered, React removes it — and all of its state goes with it. Bring it back and it starts fresh.

Usually that is exactly right. A modal should not remember the half-typed text from last time it was open. But consider a tabbed form where each tab is a different section of a long application. Write {tab === 'address' && <AddressForm />} and a user who switches to another tab and back finds their address fields blank. Nothing is broken in your code; the component was destroyed and rebuilt.

You have two ways out. Lift the values into the parent so they survive the child being unmounted, which is the better answer when the data matters — it is the same lifting-state-up pattern from the state lesson. Or keep the component mounted and hide it with CSS, which preserves everything including scroll position, at the cost of rendering work you are not showing. Choose deliberately rather than discovering the difference through a bug report.

Finally, for choosing between more than two or three options, an object lookup reads far better than a chain of ternaries. Define a map from a key to a component and index into it, with a sensible fallback for unknown keys.

Example
// Unmounted: state is destroyed when the tab changes
{tab === 'address' && <AddressForm />}

// Kept mounted: state survives, at the cost of always rendering it
<div style={{ display: tab === 'address' ? 'block' : 'none' }}>
  <AddressForm />
</div>

// Many options: a lookup object beats stacked ternaries
const TAB_CONTENT = {
  personal: PersonalForm,
  address: AddressForm,
  documents: DocumentUpload
};

function Application({ tab }) {
  const Content = TAB_CONTENT[tab] ?? NotFoundTab;   // capital C: it is a component
  return <Content />;
}
Notes
  • The variable holding a component must start with a capital letter — const Content = ... then <Content />. Lowercase, and JSX treats it as an HTML tag name and renders nothing useful.
Ask AI