Skip to content
aviral gupta

// B2.4 · ~30 min · Beginner

Conditional rendering

After this lesson you show different JSX depending on a condition with if, null, the ternary ? : and &&, without a stray 0.

Lesson 4 of 6 in B2 Describing the UI

You will be able to

  • Return different JSX with if and return, or null to render nothing
  • Choose between two pieces of JSX with the ternary ? : inside JSX
  • Include JSX with && without a stray 0, or build it in a let variable
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson B2.3: children and a default value. What text does App show?

    function Label({ color = 'gray', children }) {
      return <b>{children} ({color})</b>;
    }
    
    function App() {
      return <Label>Sale</Label>;
    }
  2. Predict · Activity 2 of 7

    Predict before you read on. There are no new messages. What text does Inbox show?

    const messageCount = 0;
    
    function Inbox() {
      return <div>{messageCount && <p>New messages</p>}</div>;
    }
  3. Practice · Activity 3 of 7

    Fill in the operator that shows the check mark only when isPacked is true, and nothing otherwise.

    <li>{name} {isPacked ____ '✅'}</li>
    <li>{name} {isPacked '✅'}</li>
  4. Practice · Activity 4 of 7

    Match each piece of code to what it does.

  5. Practice · Activity 5 of 7

    A parent renders <Item name="Helmet" isPacked={true} />. What HTML does it put into #root?

    function Item({ name, isPacked }) {
      let content = name;
      if (isPacked) {
        content = <del>{name}</del>;
      }
      return <li>{content}</li>;
    }
  6. Brain teaser · Activity 6 of 7

    Brain teaser. Item has an if, but no return after it. How many <li> elements does PackingList render?

    function Item({ name, isPacked }) {
      if (isPacked) {
        return <li>{name} ✅</li>;
      }
    }
    
    function PackingList() {
      return (
        <ul>
          <Item name="Tent" isPacked={true} />
          <Item name="Map" isPacked={false} />
        </ul>
      );
    }
  7. Apply · Activity 7 of 7

    Mini-task: a task list. In main.jsx, write a Task component with the props title, isDone and dueToday. A task without a title renders nothing (return null). A done task shows its title struck out with <del>, chosen with if and a let variable; every task shows ✅ or ⏳ with a ternary; a task due today adds " today" with &&. The default export renders three Tasks, one of them with an empty title, and a line about overdue tasks that must not show a stray 0 when there are none.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

Alex's packing list

A packing list in the style of react.dev's example, with all four tools. Item strikes out packed items with a ternary. Note returns null when it has no text, so the empty note leaves no trace. Summary starts a let variable with one message and replaces it when something is left. The last line uses urgent > 0 &&, so with urgent = 0 nothing appears, not a 0.

main.jsx

function Item({ name, isPacked }) {
  return (
    <li className="item">
      {isPacked ? <del>{name + ' ✅'}</del> : name}
    </li>
  );
}

function Note({ text }) {
  if (!text) {
    return null;
  }
  return <p className="note">{text}</p>;
}

function Summary({ left }) {
  let message = 'Everything is packed.';
  if (left > 0) {
    message = left + ' left to pack.';
  }
  return <p>{message}</p>;
}

export default function PackingList() {
  const urgent = 0;
  return (
    <section>
      <h1>Alex's Packing List</h1>
      <ul>
        <Item name="Tent" isPacked={true} />
        <Item name="Sleeping bag" isPacked={true} />
        <Item name="Map" isPacked={false} />
      </ul>
      <Summary left={1} />
      <Note text="" />
      {urgent > 0 && <p>{urgent} urgent items!</p>}
    </section>
  );
}

Run it with

npm run dev

Output

<!-- #root -->
<div id="root"><section><h1>Alex's Packing List</h1><ul><li class="item"><del>Tent ✅</del></li><li class="item"><del>Sleeping bag ✅</del></li><li class="item">Map</li></ul><p>1 left to pack.</p></section></div>
  • The two packed items are struck out with <del>; Map, not packed, shows only its name.
  • Note returned null for the empty text, so no <p class="note"> appears.
  • Summary reassigned its let variable, because left is 1.
  • urgent > 0 is false, and React renders false as nothing; urgent && … would have shown 0.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Exercises

Exercise 1 of 2

Strike out packed items

Every item in Alex's list looks the same. Change Item so that a packed item shows its name and a check mark, struck out with <del>, like <del>Tent ✅</del>, and an item that is not packed shows only its name. Use the ternary ? : inside the <li>. Keep the name Item and its export: the tests render it with their own props.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Hints
  1. Hint 1

    Inside the <li>, write {isPacked ? … : …}: the first part for packed items, the second for the others.

  2. Hint 2

    The packed part is JSX: <del>{name + ' ✅'}</del>. The other part is just name.

  3. Hint 3

    The whole line: {isPacked ? <del>{name + ' ✅'}</del> : name}.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function Item({ name, isPacked }) {
  return (
    <li className="item">
      {isPacked ? <del>{name + ' ✅'}</del> : name}
    </li>
  );
}

export default function PackingList() {
  return (
    <section>
      <h1>Alex's Packing List</h1>
      <ul>
        <Item name="Tent" isPacked={true} />
        <Item name="Sleeping bag" isPacked={true} />
        <Item name="Map" isPacked={false} />
      </ul>
    </section>
  );
}
Run it on your computer

Install React 19.3 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.jsx

// TODO: a packed item shows its name and a check mark, struck out with
// <del>: <del>Tent ✅</del>. An item that is not packed shows only its name.
export function Item({ name, isPacked }) {
  return <li className="item">{name}</li>;
}

export default function PackingList() {
  return (
    <section>
      <h1>Alex's Packing List</h1>
      <ul>
        <Item name="Tent" isPacked={true} />
        <Item name="Sleeping bag" isPacked={true} />
        <Item name="Map" isPacked={false} />
      </ul>
    </section>
  );
}

main.test.jsx

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {render, screen} from '@testing-library/react';
import PackingList, {Item} from './main.jsx';

test('A packed item shows its name with a check mark', () => {
  render(<Item name="Tent" isPacked={true} />);
  const item = screen.getByRole('listitem').textContent;
  assert.equal(item, 'Tent ✅', 'the item reads ' + JSON.stringify(item));
});

test('A packed item is struck out with <del>', () => {
  render(<Item name="Tent" isPacked={true} />);
  const del = screen.getByRole('listitem').querySelector('del');
  assert.ok(del, 'there is no <del> in the packed item');
  assert.equal(del.textContent, 'Tent ✅', 'the <del> reads ' + JSON.stringify(del.textContent));
});

test('An item that is not packed shows only its name', () => {
  render(<Item name="Map" isPacked={false} />);
  const item = screen.getByRole('listitem');
  assert.equal(item.textContent, 'Map', 'the item reads ' + JSON.stringify(item.textContent));
  assert.equal(item.querySelector('del'), null, 'the item that is not packed is struck out');
});

test('The list strikes out two of its three items', () => {
  render(<PackingList />);
  const struck = screen.getAllByRole('listitem').filter((li) => li.querySelector('del')).length;
  assert.equal(struck, 2, 'struck-out items: ' + struck);
});

In a Vite React project (npm create vite@latest, template react), put main.jsx in src/ as App.jsx and start it with npm run dev. React renders it inside <StrictMode>, as here.

Run the program:

npm run dev

There is no command for the checks on your computer yet. They are in main.test.jsx, written with node:test and React Testing Library.

Exercise 2 of 2

An inbox without a stray 0

Inbox shows how many new messages there are. Run it: the empty inbox shows a 0 under its heading, and one message is called "messages". Fix both: with 0 messages, show nothing below the heading; with 1, write "You have 1 new message"; otherwise "You have 3 new messages". Keep the name Inbox and its export.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Hints
  1. Hint 1

    count && … gives 0 when count is 0, and React shows the number. Put a boolean on the left.

  2. Hint 2

    count > 0 && <p>…</p> renders nothing for 0, because count > 0 is false.

  3. Hint 3

    For the word, use a ternary inside the paragraph: {count === 1 ? 'message' : 'messages'}.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function Inbox({ count }) {
  return (
    <div>
      <h2>Inbox</h2>
      {count > 0 && <p>You have {count} new {count === 1 ? 'message' : 'messages'}</p>}
    </div>
  );
}

export default function App() {
  return (
    <>
      <Inbox count={3} />
      <Inbox count={1} />
      <Inbox count={0} />
    </>
  );
}
Run it on your computer

Install React 19.3 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.jsx

// TODO: with 0 messages, show nothing below the heading (no stray 0).
// With 1 message, say "message"; otherwise "messages".
export function Inbox({ count }) {
  return (
    <div>
      <h2>Inbox</h2>
      {count && <p>You have {count} new messages</p>}
    </div>
  );
}

export default function App() {
  return (
    <>
      <Inbox count={3} />
      <Inbox count={1} />
      <Inbox count={0} />
    </>
  );
}

main.test.jsx

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {render, screen} from '@testing-library/react';
import {Inbox} from './main.jsx';

test('3 messages: You have 3 new messages', () => {
  render(<Inbox count={3} />);
  const line = screen.getByText(/^You have/).textContent;
  assert.equal(line, 'You have 3 new messages', 'the line reads ' + JSON.stringify(line));
});

test('1 message: You have 1 new message', () => {
  render(<Inbox count={1} />);
  const line = screen.getByText(/^You have/).textContent;
  assert.equal(line, 'You have 1 new message', 'the line reads ' + JSON.stringify(line));
});

test('0 messages: only the heading, no stray 0', () => {
  const {container} = render(<Inbox count={0} />);
  assert.equal(container.textContent, 'Inbox', 'the inbox reads ' + JSON.stringify(container.textContent));
});

In a Vite React project (npm create vite@latest, template react), put main.jsx in src/ as App.jsx and start it with npm run dev. React renders it inside <StrictMode>, as here.

Run the program:

npm run dev

There is no command for the checks on your computer yet. They are in main.test.jsx, written with node:test and React Testing Library.

Common mistakes

Writing an if statement inside JSX braces

function Item({ name, isPacked }) {
  return (
    <li className="item">
      {if (isPacked) { name + ' ✅' }}
    </li>
  );
}

export default function PackingList() {
  return (
    <ul>
      <Item name="Tent" isPacked={true} />
    </ul>
  );
}

What React prints

error TS1109: Expression expected.

Why, and the fix

Curly braces in JSX take an expression, and if is a statement, so the JSX transform stops and nothing runs. The full line starts with the position, main.jsx(4,8), and a second error, TS1381, follows. Use the ternary inside the braces, {isPacked ? name + ' ✅' : name}, or put the if before the return.

Leaving out the otherwise part of a ternary

function Item({ name, isPacked }) {
  return <li className="item">{name} {isPacked ? '✅'}</li>;
}

export default function PackingList() {
  return (
    <ul>
      <Item name="Tent" isPacked={true} />
    </ul>
  );
}

What React prints

error TS1005: ':' expected.

Why, and the fix

The conditional operator always has three parts: condition ? then : otherwise. The full line starts with main.jsx(2,53). Add the otherwise value, {isPacked ? '✅' : ''}, or, when there is nothing to show otherwise, use && instead: {isPacked && '✅'}.

Declaring the JSX variable with const

function Item({ name, isPacked }) {
  const content = name;
  if (isPacked) {
    content = name + ' ✅';
  }
  return <li className="item">{content}</li>;
}

export default function PackingList() {
  return (
    <ul>
      <Item name="Tent" isPacked={true} />
    </ul>
  );
}

What React prints

TypeError: Assignment to constant variable.

Why, and the fix

A const cannot be reassigned, so the if throws while Tent renders, and the run prints Uncaught TypeError (Chrome and Node wording; Firefox and Safari word it differently). Declare the variable with let, as react.dev does: let content = name;.

React in the browser: React 19.3.0 (development build) and React Testing Library, MIT, served from this site; JSX is compiled by TypeScript 6.0.3, Apache-2.0. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Conditions are plain JavaScript

React has no special syntax for conditions: you use JavaScript. A component can test a prop with if and return different JSX: if (isPacked) { return <li>{name} ✅</li>; } return <li>{name}</li>;. To render nothing at all, return null. react.dev notes that this is not common, because it may surprise whoever renders the component; more often the parent decides whether to include it. An if statement cannot go inside the JSX braces: braces take an expression, and if is a statement, so the JSX transform stops.

The ternary ? : chooses inside JSX

When two branches differ only in a small part, the conditional operator keeps the JSX in one place: <li>{isPacked ? name + ' ✅' : name}</li>. Read it as: if isPacked, then this, otherwise that. Each side can be JSX too: {isPacked ? <del>{name}</del> : name}. Both the if version and the ternary describe the same result; JSX elements are only descriptions. A ternary always needs both parts: without the : and the otherwise-value, the code does not compile. For deeper nesting, react.dev suggests extracting child components.

&& includes or skips, and let collects

{isPacked && '✅'} shows the check mark when isPacked is true and nothing when it is false: React treats false, null and undefined as holes and renders nothing. But never put a number on the left of &&: with count = 0, {count && <p>New</p>} renders 0. Write a boolean, count > 0 &&. When shortcuts get in the way, use a variable: let content = name; if (isPacked) { content = <del>{name}</del>; } and then {content} in the JSX. It needs let, because const cannot be reassigned.

Sources

Last reviewed October 5, 2026