DEV Community

Kévin Rambonona
Kévin Rambonona

Posted on

React: Building a Better Alternative to "condition && <Component />"

Introduction

In this article, I'll start with a seemingly simple problem that led me to explore some more complex avenues of thought than I expected. Rather than directly providing you with the final solution, I wanted to share my journey and highlight these different points.

Note: This article was originally written in French and translated to English using Gemini and Claude, as it's easier for me to write in my native language.

You can find the complete code in this GitHub repository:

https://github.com/krambono/show

The Problem

When I use React (and JSX), I don't like conditional renderings of this kind:

  • condition && <Component />
  • condition ? <Component /> : null

It's purely subjective, but I find that it makes the code less readable.

Here's an example you'll likely recognize:

const PostPage = () => {
  const { data, error, isLoading } = useQuery();

  return (
    <Layout>
      {isLoading && <p>Loading post...</p>}

      {error ? (
        <>
          <h2 style={{ color: 'red' }}>Error</h2>
          <p>Unable to retrieve post.</p>
          <pre>{error.message}</pre>
        </>
      ) : null}

      {data && (
        <article>
          <h1>{data.title}</h1>
          <p>{data.content}</p>
        </article>
      )}
    </Layout>
  );
};
Enter fullscreen mode Exit fullscreen mode

I find the syntax heavy and not very explicit. It doesn't clearly highlight the condition and its result. You have to follow the chain of braces or parentheses to fully understand what's included and what isn't.

For example, I have a clear preference for Svelte or Angular syntaxes that offer if - else blocks in their templates.

Let's Create a Simple Component

A first solution to improve this display is to create a Show component that takes a condition and JSX children as props, which will be displayed based on the condition.

type Props = {
  condition: unknown;
  children: ReactNode;
};

const Show: FC<Props> = ({ condition, children }) => 
  condition ? children : null;
Enter fullscreen mode Exit fullscreen mode

Note: The type of condition is unknown because we want to be able to pass any expression as a condition. For example, a data variable that might be undefined.

Simple, isn't it?

Going back to our previous example, it looks like this:

const PostPage = () => {
  const { data, error, isLoading } = useQuery();

  return (
    <Layout>
      <Show condition={isLoading}>
        <p>Loading post...</p>
      </Show>

      <Show condition={error}>
        <h2 style={{ color: 'red' }}>Error</h2>
        <p>Unable to retrieve post.</p>
        <pre>{error.message}</pre>
      </Show>

      <Show condition={data}>
        <article>
          <h1>{data.title}</h1>
          <p>{data.content}</p>
        </article>
      </Show>
    </Layout>
  );
};
Enter fullscreen mode Exit fullscreen mode

I find this much better!

... but there are two problems, one of which is major:

  • Eager evaluation of children
  • TypeScript doesn't narrow the type of the condition

Eager Evaluation of JSX

Let's revisit our previous example, simplified to highlight the part we're interested in.

const PostPage = () => {
  const { data, error, isLoading } = useQuery();

  return (
    <Show condition={data}>
      <article>
        <h1>{data.title}</h1>
        <p>{data.content}</p>
      </article>
    </Show>
  );
};
Enter fullscreen mode Exit fullscreen mode

If the data variable is initially undefined, what do you think the behavior of the PostPage component will be?

An error will be thrown: you cannot access the title and content properties of the data variable when it's undefined...

And yet, the condition passed to the Show component should ensure that data is defined before accessing it, right?

The problem is that the children of the Show component are evaluated immediately, even before the component renders.

This seems logical: React components are functions that have parameters (props) and return React elements.

And as in JavaScript, the arguments of a function are evaluated before that function is called; the JSX passed as children is evaluated even before entering the Show component.

This is called eager evaluation.

But couldn't we defer the evaluation of our children until we actually need them?

Lazy Evaluation

How can we evaluate our children only when the condition is true?

What if we used a function? 🤔

Instead of directly receiving JSX as props, we could pass a function that returns that JSX. This would allow it to be executed only when our condition is true.

We then get the following code:

type Props = {
  condition: unknown;
  children: () => ReactNode;
};

export const Show: FC<Props> = ({ condition, children }) => 
  condition ? children() : null;

const PostPage = () => {
  const { data, error, isLoading } = useQuery();

  return (
    <Show condition={data}>
      {() => (
        <article>
          <h1>{data!.title}</h1>
          <p>{data!.content}</p>
        </article>
      )}
    </Show>
  );
};
Enter fullscreen mode Exit fullscreen mode

It works! 🎉

Deferring the evaluation of our variable until we actually need it is called lazy evaluation.

To accept both a function and JSX directly, we can rewrite our Show component like this:

export const Show: FC<Props> = ({ condition, children }) => {
  if (!condition) {
    return null;
  }
  return typeof children === 'function' ? 
    children() : children;
};
Enter fullscreen mode Exit fullscreen mode

TypeScript Doesn't Narrow the Type of the Condition

Let's go back to our example:

const PostPage = () => {
  const { data, error, isLoading } = useQuery();

  return (
    <Show condition={data}>
      {() => (
        <article>
          <h1>{data!.title}</h1>
          <p>{data!.content}</p>
        </article>
      )}
    </Show>
  );
};
Enter fullscreen mode Exit fullscreen mode

You must have noticed the use of the Non-null assertion operator ! to access the properties of data: data!.title and data!.content.

TypeScript doesn't perform type narrowing on the data variable in this context. Even though data is evaluated as "truthy" in the condition, its type remains unchanged inside the children function. TypeScript continues to consider it as potentially undefined. That's why one solution is to use the ! operator.

Enter Generics 😎

To have something more robust, we can use generic types.

Instead of using an unknown type for our condition, we use a type T that we re-inject as a parameter to our children function by declaring it as non-null:

type Props<T> = {
  condition: T;
  children: ReactNode | ((value: NonNullable<T>) => ReactNode);
};

const Show = <T,>({ condition, children }: Props<T>) => {
  if (!condition) {
    return null;
  }

  return typeof children === 'function' ? 
    children(condition) : children;
};

const PostPage = () => {
  const { data, error, isLoading } = useQuery();

  return (
    <Show condition={data}>
      {(definedData) => (
        <article>
          <h1>{definedData.title}</h1>
          <p>{definedData.content}</p>
        </article>
      )}
    </Show>
  );
};
Enter fullscreen mode Exit fullscreen mode

The ! operator is no longer necessary, and we gain type safety!

Conclusion

Using the latest version of our Show component and revisiting our first example, we get the following result:

export const PostPage = () => {
  const { data, error, isLoading } = useQuery();

  return (
    <Layout>
      <Show condition={isLoading}>
        <p>Loading post...</p>
      </Show>

      <Show condition={error}>
        {definedError => (
          <>
            <h2 style={{ color: 'red' }}>Error</h2>
            <p>Unable to retrieve post.</p>
            <pre>{definedError.message}</pre>
          </>
        )}
      </Show>

      <Show condition={data}>
        {definedData => (
          <article>
            <h1>{definedData.title}</h1>
            <p>{definedData.content}</p>
        </article>
        )}
      </Show>
    </Layout>
  );
};
Enter fullscreen mode Exit fullscreen mode

Now let's take a step back and recall our main objective: to improve readability.

Have we achieved our goal?

What do you think? 😉


How do you handle conditional rendering in your React components?

I'd be curious to discuss it with you.

Top comments (1)

Collapse
 
gaels profile image
GaelS

I really like this pattern and use it regularly. You just need to read the JSX to clearly understand what's going on <3