Conditional Rendering In React - With A Focus On Readability And Clean Code
Tags: react, javascript, webdev, frontend
Published: 2022-11-04 13:34:38

---
description: There are many ways to conditionally render in React. But what are the best options regarding readability? Here's when to use early returns, ternary, &&, and others.
---

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](#early-return)
2. [Optional Chaining Operator ?](#optional-chaining-operator-)
3. [Ternary](#ternary)
4. [Logical AND Operator &&](#logical-and-operator-ampamp)
5. [Return null](#return-null)
6. [Render Variables](#render-variables)
7. [Component Map / Enum](#component-map-enum)

## Early return

My personal favorite is “[returning early and often](https://medium.com/swlh/return-early-pattern-3d18a41bba8)”. Instead of using conditionals inside your JSX you can use simple `if` statements.

```typescript
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>
  );
}
```

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:

```typescript
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>
  );
}
```

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.

```typescript
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>
  );
}
```

## Optional Chaining Operator ?

When you want to **render a list only if an array exists** the [optional chaining operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) `?` 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. 

```typescript
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>
  );
}
```

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

```typescript
// 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>
```

## Ternary

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

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

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

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.

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

  return (
    <Layout>
      {isLoading ? (
        <Loading>
          {
            // lots of JSX here
          }
        </Loading>
      ) : (
        <Content>
          {
            // lots of JSX here
          }
        </Content>
      )}
    </Layout>
  );
```

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.

```typescript
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>
  );
}
```

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. 

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

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

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`.

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

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

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.

```typescript
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>
  );
}
```

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.

```typescript
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>
  );
}
```

## 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 `&&`.

```typescript
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>
  );
}
```

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

```typescript
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>
  );
}
```

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.

```typescript
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>
  );
}
```

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.

```typescript
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>;
}
```

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

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

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

  return <Layout>{content}</Layout>;
}
```

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.

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

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

  return <Layout>{components[status]}</Layout>;
}
```

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

```typescript
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>;
}
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dxgsm802cn0dosanisfa.png)](https://profy.dev/email-course-signup)