DEV Community

Cover image for Conditional Rendering In React - With A Focus On Readability And Clean Code
Johannes Kettmann
Johannes Kettmann

Posted on • Originally published at profy.dev

Conditional Rendering In React - With A Focus On Readability And Clean Code

There are lots of different ways to conditionally render components in React: if … else, early returns, ternaries, logical ANDs (only to name a few).

It’s obviously important that you understand the most common ones. But it’s also important to know how and when each of them can help you to write cleaner code. For example, the ternary operator might be popular. But as soon as you use multiple conditions and chain ternaries together you’re in for a spaghetti feast.

On this page, you can learn the most common ways to conditionally render components in React. Not only that. You can also learn about different situations when a certain approach can be dangerous while another one can lead to clean code.

Table Of Contents

  1. Early return
  2. Optional Chaining Operator ?
  3. Ternary
  4. Logical AND Operator &&
  5. Return null
  6. Render Variables
  7. Component Map / Enum

Early return

My personal favorite is “returning early and often”. Instead of using conditionals inside your JSX you can use simple if statements.

function ListPage() {
  const { data, error, isLoading } = useGetData();

  if (isLoading) {
    return (
      <Layout>
        <LoadingSpinner />
      </Layout>
    );
  }

  if (error) {
    return (
      <Layout>
        <ErrorMessage message={error.message} />
      </Layout>
    );
  }

  return (
    <Layout>
      {
        data.items.map((item) => (
          <Item key={item.id} item={item} />
        ))
      }
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

There might be a bit of code duplication (in the above example the repeated <Layout> wrapper). But early returns make the program flow easy to follow. For example, if there’s a bug with the loading screen you can likely stop reading at the very first if statement and ignore the rest of the code.

Note that you don’t need an else statement. This saves you one indentation level in the last return. This again makes the JSX easier to read especially if it’s deeply nested.

In some cases, early return statements may not seem possible. For example, the components that should be rendered conditionally may have a lot of sibling components:

function ListPage() {
  const { data, isLoading } = useGetData();

  if (isLoading) {
    return (
      <Layout>
        <LoadingSpinner />
        <MoreComponents>
          {
            //... more components here
          }
        </MoreComponents>
      </Layout>
    );
  }

  return (
    <Layout>
      {data.items.map((item) => (
        <Item key={item.id} item={item} />
      ))}
      <MoreComponents>
        {
          //... more components here
        }
      </MoreComponents>
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this case, it’s tedious and error-prone to duplicate the sibling <MoreComponents> and its children.

But often we can create a separate component and use early returns there.

export function ItemList() {
  const { data, error, isLoading } = useGetData();

  if (isLoading) {
    return (
      <LoadingSpinner />
    );
  }

  return data.items.map((item) => <Item key={item.id} item={item} />);
}

export function ListPage() {
  return (
    <Layout>
      <ItemList />
      <MoreComponents>
        {
          //... more components here
        }
      </MoreComponents>
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

Optional Chaining Operator ?

When you want to render a list only if an array exists the optional chaining operator ? is a great fit.

Let’s assume we render a blog post that may or may not have a list of tags. If it has tags we show them. If not we simply don’t render anything.

function ListPage() {
  // data.tags may be null or undefined
  const { data } = useGetData();

  return (
    <Layout>
            <Post post={data.post} />
      {data.tags?.map((tag) => (
        <Tag key={tag.id} tags={tag} />
      ))}
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

If data.tags is null or undefined React renders undefined (aka nothing) otherwise it renders the tags. Translated into code this means.

// data.tags is null or undefined
<Layout>
    <Post post={data.post} />
  {undefined}
</Layout>

// data.tags defined
<Layout>
    <Post post={data.post} />
  <Tag key="tag1" tags={tag1} />
  <Tag key="tag2" tags={tag2} />
  <Tag key="tag3" tags={tag3} />
</Layout>
Enter fullscreen mode Exit fullscreen mode

Ternary

When you have a simple “if … else” situation with a single condition, a ternary operator can be a good fit.

function ListPage() {
  const { data, isLoading } = useGetData();

  return (
    <Layout>
      {isLoading ? (
        <LoadingSpinner />
      ) : (
        data.items.map((item) => <Item key={item.id} item={item} />)
      )}
    </Layout>
  );
Enter fullscreen mode Exit fullscreen mode

This is quite easy to read if formatted well (as Prettier did above) and the return values are short. If the inner JSX gets more complex the ternary starts to become hard to read.

function ListPage() {
  const { data, isLoading } = useGetData();

  return (
    <Layout>
      {isLoading ? (
        <Loading>
          {
            // lots of JSX here
          }
        </Loading>
      ) : (
        <Content>
          {
            // lots of JSX here
          }
        </Content>
      )}
    </Layout>
  );
Enter fullscreen mode Exit fullscreen mode

Ternaries also become hard to read you have a “if … else if … else” situation. Now you need to chain the ternary operators which can be very hard to follow.

Here is a relatively simple example where it’s already easy to miss the second condition.

function ListPage() {
  const { data, isLoading, error } = useGetData();

  return (
    <Layout>
      {isLoading ? (
        <LoadingSpinner />
      ) : error ? (
        <ErrorMessage message={error.message} />
      ) : (
        data.items.map((item) => <Item key={item.id} item={item} />)
      )}
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this situation, it’s better to use early returns or the logical AND operator as you’ll see in a bit.

Logical AND Operator &&

When you want to render a component if a condition is met and nothing instead the logical AND operator && is a good fit.

function ListPage() {
  const { data, error } = useGetData();

  return (
    <Layout>
      {error && <ErrorMessage message={error.message} />}
      {data.items.map((item) => (
        <Item key={item.id} item={item} />
      ))}
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

Note that some falsy values are rendered by React. In this example, you will either see the list of items as expected or the number 0.

 function ListPage() {
  const { data, error } = useGetData();

  return (
    <Layout>
      {data.items.length && data.items.map((item) => (
        <Item key={item.id} item={item} />
      ))}
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

So in doubt, you should either

  • double negate the value !!data.items.length
  • use a boolean check like data.items.length > 0
  • case to a boolean with Boolean(error).

A problem you can often see when using the logical AND operator (as well as ternary operators) is chained conditions. Look how awful this is to read even though this is still a quite simple example.

function ListPage() {
  const { data, isLoading, error } = useGetData();

  return (
    <Layout>
            {isLoading && <LoadingSpinner />}
      {!isLoading && !!error && <ErrorMessage message={error.message} />}
      {!isLoading && !error && !!data && data.items.map((item) => (
        <Item key={item.id} item={item} />
      ))}
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

To improve the readability of this code we can extract the chained conditions into variables. Now the conditions inside the JSX are short. The reader doesn’t have to follow what exactly showData means to understand the code. But they’re free to investigate the condition if necessary.

This is also the alternative to chained ternary operators mentioned in the section above.

function ListPage() {
  const { data, isLoading, error } = useGetData();

  const showError = !isLoading && !!error;
  const showData = !isLoading && !error && !!data;

  return (
    <Layout>
            {isLoading && <LoadingSpinner />}
      {showError && <ErrorMessage message={error.message} />}
      {showData && data.items.map((item) => (
        <Item key={item.id} item={item} />
      ))}
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

Return null

When you want to hide a component based on a condition returning null can be a good alternative to the logical AND operator &&.

We can either let the parent decide to hide the component with &&.

export function ItemList({ items }) {
  return items.map((item) => <Item key={item.id} item={item} />);
}

function ListPage() {
  const { data } = useGetData();
  return (
    <Layout>
      {!!data?.items && <ItemList items={data.items} />}
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

Or we make the hiding the responsibility of the child component.

export function ItemList({ items }) {
  if (!items) {
    return null;
  }

  return items.map((item) => <Item key={item.id} item={item} />);
}

function ListPage() {
  const { data } = useGetData();
  return (
    <Layout>
      <ItemList items={data?.items} />
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

Whether to use the && operator or return null is a case-to-case decision. The question is whether it should be the parent’s or the component’s own responsibility to render based on the condition.

Note that returning null from the child can also have an impact on performance. If the child contains a lot of logic or sends API requests the following code wouldn’t be ideal.

export function ItemList({ isHidden }) {
    const { data } = useGetData();

  if (isHidden) {
    return null;
  }

  return data.items.map((item) => <Item key={item.id} item={item} />);
}

function ListPage({ isItemListHidden }) {
  return (
    <Layout>
      <ItemList isHidden={isItemListHidden} />
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

Since we can only return from the component after all hooks we would send an API request even though the component isn’t visible. Which probably is not what we want.

Render Variables

Another alternative is assigning components to variables and adding them to the final JSX. You can use different styles. Here is my favorite as it supports multiple conditions and is relatively easy to read.

function ListPage() {
  const { data, isLoading } = useGetData();

  let content = <LoadingSpinner />
  if (!isLoading) (
    content = data.items.map((item) => <Item key={item.id} item={item} />)
  );

  return <Layout>{content}</Layout>;
}
Enter fullscreen mode Exit fullscreen mode

Obviously, you can also use other options like a ternary operator.

function ListPage() {
  const { data, isLoading } = useGetData();

  const content = isLoading ? (
    <LoadingSpinner />
  ) : (
    data.items.map((item) => <Item key={item.id} item={item} />)
  );

  return <Layout>{content}</Layout>;
}
Enter fullscreen mode Exit fullscreen mode

I’m personally not a big fan of these render variables. From my perspective, they encourage the mixing of business logic with UI code. When you start reading the component from top to bottom you’re like “Hey here’s the business logic, wait there’s a bit of JSX, and again business logic, and again JSX…”.

Component Map / Enum

If you have a condition based on e.g. a string you might also use an enum object. You can use the possible values as keys of an object and the corresponding components as their values.

const components = {
  "loading": <LoadingSpinner />,
  "error": <ErrorMessage />,
  "success": <ItemList />
}

function ListPage() {
  const { status } = useGetData();

  return <Layout>{components[status]}</Layout>;
}
Enter fullscreen mode Exit fullscreen mode

This gets a bit more verbose when you want to pass props to the components.

const components = {
  "loading": () => <LoadingSpinner />,
  "error": ({ error }) => <ErrorMessage message={error.message} />,
  "success": ({ data }) => <ItemList items={data.items} />
}

function ListPage() {
  const { data, error, status } = useGetData();

  return <Layout>{components[status]({ data, error })}</Layout>;
}
Enter fullscreen mode Exit fullscreen mode

Honestly, I’ve never used this nor have I seen it out in the wild. But I wanted to share this for completion.

7-Day Email Course

Top comments (0)