Get eBook BundleVolume I index
Components Before Abstractions

Conditional UI in the First App

Ishtmeet Singh @ishtms/July 20, 2026/12 min read
#react#conditional-rendering#components#props#jsx

The components we've been writing have mostly been returning the same UI every time. Different text maybe, different props, but the structure itself wasn't really changing much.

Real UIs don't stay that simple for very long though.

A lesson can be not started yet, currently being read, or already complete. Some lessons might not be published at all. Maybe a lesson is new and gets a little badge, maybe it doesn't. And depending on all this data, our component needs to return different UI.

Something as small as this already gives us two possible results:

jsx
<LessonCard complete={false} />
<LessonCard complete />

React isn't going to find the already-rendered card and start toggling random DOM nodes after this prop changes. The component runs for the current props and returns whatever UI those props describe.

So conditional rendering in React is mostly just... JavaScript conditions deciding which React elements get returned.

Nothing too exotic going on there.

Start with the UI states first

Before writing ternaries everywhere, first figure out which states the UI can actually have.

For our lesson, let's say we've got these three:

text
not-started
reading
complete

And each one should show something specific.

StatusLabelAvailable action
not-startedNot startedStart lesson
readingIn progressContinue lesson
completeCompleteReview lesson

I like doing this before touching JSX because otherwise it's very easy to start adding booleans as you go and only later realize you've created combinations that don't make much sense.

If there are only two possibilities, a boolean can be perfectly fine. complete={true} or complete={false} is easy enough.

But once you've got three mutually exclusive states, a named value usually reads better.

jsx
<LessonCard status="reading" />

Now you can actually see what state the lesson is in without having to decode three different booleans in your head.

Returning nothing is still a valid result

Suppose draft lessons shouldn't appear for readers at all.

You can handle that right at the beginning of the component:

jsx
function LessonCard({ lesson }) {
  if (!lesson.published) {
    return null;
  }

  return <article>{lesson.title}</article>;
}

null means this component doesn't produce any host UI for this render.

And once that first if returns, the rest of the function already knows the lesson is published. There's no reason to wrap the remaining code in an else.

You could write this:

jsx
function LessonCard({ lesson }) {
  if (!lesson.published) {
    return null;
  } else {
    return <article>{lesson.title}</article>;
  }
}

But the else isn't doing anything useful. If the first branch ran, the function already ended.

I tend to use these early returns when one condition changes the entire result. Maybe required data is missing, maybe we're showing a full loading screen, maybe the user doesn't have access to something, or maybe there simply shouldn't be any output.

One small detail though: returning null doesn't mean React somehow forgets the component exists. React still rendered the component and got null back. It just has no DOM output to create for it on that render.

Picking between two values

Now let's say the component always renders a paragraph, but the text inside depends on a boolean.

This is a nice place for a conditional expression:

jsx
function LessonStatus({ complete }) {
  return (
    <p>{complete ? "Complete" : "Not started"}</p>
  );
}

If complete is truthy, we get "Complete". Otherwise we get "Not started".

And both sides don't have to be strings either. They can be React elements.

jsx
const status = complete
  ? <strong>Complete</strong>
  : <span>Not started</span>;

Then render whatever got selected:

jsx
return <p>Status: {status}</p>;

I usually prefer pulling it into a variable once the JSX starts getting annoying to read. You can technically fit a lot of logic between {} in JSX, but being able to fit it there doesn't mean it'll still be pleasant two weeks later.

Please don't build a giant ternary chain

Three states can technically be written like this:

jsx
const label = complete
  ? "Complete"
  : started
    ? "In progress"
    : "Not started";

Does it work? Yep.

Do I want to read five more states added to this? Absolutely not.

The problem gets worse because now you have to work out which : belongs to which condition while reading the code. For a tiny expression it's okay. Once you're modelling a real status, just give that status a name and use it directly.

For labels, an object works really well:

jsx
const labels = {
  "not-started": "Not started",
  reading: "In progress",
  complete: "Complete",
};

Then:

jsx
const label = labels[status];

Much easier to scan.

If every status needs more than one decision, a switch can also make sense.

jsx
function getActionLabel(status) {
  switch (status) {
    case "not-started":
      return "Start lesson";

    case "reading":
      return "Continue lesson";

    case "complete":
      return "Review lesson";

    default:
      throw new Error(`Unknown lesson status ${status}`);
  }
}

That default is useful during development too. If some API suddenly sends "finished-ish" or somebody passes a status you never supported, I'd rather get an obvious error than quietly render undefined somewhere and then wonder why the button text vanished.

Showing something only when it's needed

Sometimes there aren't two visible states. You either want a piece of UI or you want nothing there.

A "New" badge is a good example.

jsx
function LessonTitle({ title, isNew }) {
  return (
    <h2>
      {title}
      {isNew && <span>New</span>}
    </h2>
  );
}

If isNew is true, the expression produces the <span> and React renders it. If it's false, the expression produces false, which React doesn't display.

That's really what && is nice for in JSX: render this thing when the condition passes, otherwise render nothing.

If both outcomes should show something, use a ternary instead:

jsx
{complete ? <CompleteBadge /> : <ReadingBadge />}

Now there are two actual outputs, so writing both of them directly makes more sense.

Be careful with numbers and &&

There's a small gotcha here which looks harmless until you see a random 0 sitting in your UI.

Say we write:

jsx
function RemainingCount({ count }) {
  return <p>{count && <span>{count} remaining</span>}</p>;
}

You might read that as "if count has a value, show the span."

But JavaScript doesn't return a boolean from &&. It returns one of the actual operands.

So if count is 0, this:

js
count && <span>...</span>

evaluates to:

js
0

And React does render numbers.

Meaning you can end up with a visible 0 where you expected nothing.

Better to say what condition you actually mean:

jsx
function RemainingCount({ count }) {
  return (
    <p>{count > 0 && <span>{count} remaining</span>}</p>
  );
}

Now count > 0 gives us a real boolean, and false produces no visible output.

Strings can give you similar little questions. An empty string is falsy, but maybe an empty string is valid data in your app. So before using some value directly as a condition, make sure its truthiness actually means the same thing your UI condition means.

Not rendered and hidden are different

Suppose we have lesson details:

jsx
{showDetails && <LessonDetails />}

When showDetails is false, LessonDetails isn't included in that returned React output. Its DOM nodes aren't sitting there hidden somewhere. React leaves that component out for that render.

Now compare that with:

jsx
<div hidden={!showDetails}>
  <LessonDetails />
</div>

Here the element still exists in the DOM. We're using the browser's hidden behavior to stop it being presented normally.

Those are different decisions.

Sometimes you genuinely don't need the component around, so conditionally rendering it makes sense. Other times you may want its DOM to stay there because you're preserving something that depends on it remaining mounted.

There's also component state to consider. If React removes a component and later creates it again, you're dealing with a new rendered instance in that position. A DOM node that merely stays rendered while hidden hasn't been removed.

So don't choose between hiding and conditional rendering only because one syntax is shorter. Decide whether the component should exist for that state.

One status is usually better than three conflicting booleans

This kind of component API starts looking suspicious pretty quickly:

jsx
<LessonCard
  started
  complete
  locked
/>

Okay... so it's started, also complete, and also locked?

Maybe your app has a reason for that combination, but quite often these booleans are all trying to describe one underlying status.

And once you have several booleans doing that, you have to decide what every combination means. What happens with started={false} and complete={true}? What about complete and locked together? Which one wins when the UI picks a button label?

A single status avoids a lot of this:

jsx
<LessonCard status="complete" />

Now only one mutually exclusive state is active.

Independent facts can still stay as booleans:

jsx
<LessonCard status="complete" featured />

A lesson can be complete and featured at the same time. No problem there because those values describe different facts.

So booleans themselves aren't bad. Problems start when several booleans are secretly trying to represent one thing.

Loading, error, empty, and actual content

Data fetching gives us another place where conditional UI gets messy if we don't model the cases properly.

Imagine we're loading lessons from an API.

There are at least a few outcomes we probably care about. The request might still be loading. It might have failed. It might have succeeded but returned zero lessons. Or it might have succeeded with actual lessons.

Those are not the same UI state.

An empty array shouldn't automatically mean "loading", because a successfully loaded course can also genuinely have zero lessons.

Early returns make this pretty readable:

jsx
if (loading) return <p>Loading lessons</p>;
if (error) return <p>Lessons could not be loaded</p>;
if (lessons.length === 0) return <p>No lessons available</p>;

If execution gets past all three lines, we already know we have loaded data and the list isn't empty. So the remaining JSX can just deal with rendering lessons.

Again, we don't need one giant expression trying to handle every case inside the return statement.

Conditional returns and Hooks

There is one React rule you need to remember before putting early returns anywhere you feel like.

Hooks need to be called in the same order between renders.

So this can cause a problem:

jsx
function LessonDetails({ available }) {
  if (!available) return null;

  const [open, setOpen] = useState(false);
  // ...
}

Imagine available is false on one render. React returns before reaching useState().

Then on another render available becomes true, and now React does call useState().

You've changed which Hooks run between renders, and React relies on their call order staying consistent.

Call the Hook first:

jsx
function LessonDetails({ available }) {
  const [open, setOpen] = useState(false);

  if (!available) return null;

  // ...
}

Now useState() runs on every render of this component, and the condition only decides what UI gets returned afterward.

You don't need to avoid early returns completely. Just don't put them somewhere that makes Hook calls conditional.

Putting our lesson card together

Let's use all of this in one component.

First we'll keep the labels outside the component because they don't depend on any render-specific value:

jsx
const statusLabels = {
  "not-started": "Not started",
  reading: "In progress",
  complete: "Complete",
};

Then the component:

jsx
function LessonCard({ lesson }) {
  if (!lesson.published) return null;

  const label = statusLabels[lesson.status];

  return (
    <article>
      <h2>{lesson.title}</h2>
      <p>{label}</p>
      {lesson.isNew && <span>New</span>}
    </article>
  );
}

There's actually quite a bit happening here, but none of it is very complicated.

If the lesson isn't published, we return nothing. If it is published, we pick a label from the status. And if isNew is true, we include the badge.

Three different kinds of conditions because they're answering three different questions.

I would also validate lesson.status somewhere before relying on this object forever. If some bad runtime data gives us a status that doesn't exist in statusLabels, label becomes undefined. During development you may even want to throw an error there so the bad value is obvious immediately.

Try every state

Once you've written a component with conditional UI, actually run it through every state you claim to support.

Don't only check "reading" because that's the one your test data happened to contain.

Try:

jsx
<LessonCard
  lesson={{
    title: "Conditional UI",
    published: true,
    status: "not-started",
    isNew: true,
  }}
/>

Then "reading".

Then "complete".

Then make published false.

Then turn isNew off.

You're checking whether each input produces one sensible result and whether any combinations create UI you didn't intend.

By now the component model should be getting a bit more familiar. A component receives props, runs normal JavaScript, and returns React elements. Conditional rendering doesn't introduce some new React-only condition system. You're still using if, switch, ? :, &&, objects, variables, and normal JavaScript expressions.

React only cares about the result you return for that render.

And that's really the main idea here: decide which UI states your component supports, model those states clearly, then let normal JavaScript choose the React output for the current one.