Conditional Rendering in React with a Switch Component
Tags: react, beginners, typescript, vite
Published: 2023-10-18 14:25:25

Controlling the rendering of UI elements based on certain conditions is a common task in web development. There are plenty of methods to accomplish this in React, and in this article, I'll show you how to create a simple component that does it in a declarative way.

Let's begin by setting up a React project. In this guide, I'll be using Vite, but it's important to note that for the purpose of this article, all the options, such as CRA (Create React App), Next.js, Remix, and more, are equally good.

## Creating boilerplate project

With Vite it is pretty straightforward. Lets create it with the following command:
```bash
yarn create vite
```

Then just follow the master instructions. The only note, I'd like to use Typescript here.

After creating a project (I've named it react-switch) the dependencies need to be installed with
```bash
cd ../react-switch
yarn
```

Validate boilerplate by running dev server with the following command:
```bash
yarn dev
```
and opening the link in the browser `http://127.0.0.1:5173/`. Simple and beauty Vite + React boilerplate will be shown.

And then cleanup unnecessary code and resources from the project
-  Remove unnecessary code from `App.tsx` component to get it to the following state:
```tsx
import './App.css'

function App() {
  return (
	<>
	  <h3>React Switch Component</h3>
	</>
  )
}

export default App
```
- Remove most of the styles for `App.css` and keep only the following:
```css
#root {
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;
  text-align: center;
}
```
- Beside that the following files can be removed:
	- `src/assets/react.svg`
	- `public/vite.svg`


The `never` type is used here. It represents the value which will never occur. The full info in the specification https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#the-never-type

Also we will add empty `Switch.tsx` file for our future `Switch` component in the `src` directory. 

## Switch component implementation

The component should have the structure like below when user.
```jsx
<Switch condition={<condition>}>
  <Switch.Case when={<possible-value>}>
  ...
  </Switch.Case>
  <Switch.Default>
  ...
  </Switch.Default>
</Switch>
```

We’re importing functional component (`FC`), `ReactElement`, and `ReactNode` from 'react'. These types are foundational to the `Switch` component, ensuring type safety and clarity in its usage.

```jsx
import { FC, ReactElement, ReactNode } from 'react'
```

Define the Case and Default Props. `CaseProps` and `DefaultProps` interfaces for the `Case` and `Default` elements respectively. `CaseProps` takes children and a `when` prop, which can be a string or a number (which is always possible to adjust to your specific case). `DefaultProps`, on the other hand, only accepts children - the `when` prop is set to `never`, ensuring it's not used with the `Default` component.

```jsx
interface CaseProps {
  children?: ReactNode
  when: string | number
}

interface DefaultProps {
  children?: ReactNode
  when?: never
}

```

We need one more interface which outlines the props for the main `Switch` component. It should accept a `condition` prop, which determines which `Case` component to render, and the `children` prop which could be a single or an array of `ReactElement`.

```jsx
interface SwitchComponentType extends FC<SwitchComponentProps> {
  Case: FC<CaseProps>
  Default: FC<DefaultProps>
}
```

We extend the `FC` type to declare the `Switch` component along with its nested `Case` and `Default` components. This architecture simplifies the usage syntax, making it more intuitive.

Implementation of Case and Default elements is straightforward. The main goal is to provide a wrapper for components which will be possible rendered.
```jsx
Switch.Case = ({ children }) => {
  return <>{children}</>
}

Switch.Default = ({ children }) => {
  return <>{children}</>
}
```

Now lets craft the `Switch` component itself. This is the place where the magic happens. It processes the `children` props to find the matching `Case` component based on the `condition` prop or returns the `Default` component if no match is found.

```jsx
export const Switch: SwitchComponentType = ({ condition, children }) => {
  if (!children) {
    return null
  }

  const arrayOfChildren = Array.isArray(children) ? children : [children]
  const cases = arrayOfChildren.filter((child) => child.props.when == condition)
  const defaultCases = arrayOfChildren.filter((child) => !child.props.when)

  if (defaultCases.length > 1) { 
    throw new Error('Only one <Switch.Default> is allowed')
  }

  const defaultCase = defaultCases[0]
  
  return cases.length > 0 ? <>{cases}</> : <>{defaultCase}</>
}
```

Lets break it down.

Basically as a first step we check if there are any children in our `Switch` component. It should not render anything if children are absent. 

```jsx
if (!children) {
  return null
}
```

Next line creates an `arrayOfChildren`. It checks if the `children` prop is an array. If it's an array, `arrayOfChildren` is set to `children`. If it's not an array (i.e., there's only a single child), it wraps that child in an array, making it easier to work with later in the code.

```jsx
const arrayOfChildren = Array.isArray(children) ? children : [children]
```

As a next step we create `cases` array that will contain child components whose `when` prop matches the provided `condition`. The `filter` method is used to iterate through each child component and select only those where the `when` prop is equal to the provided `condition`.

```jsx
const cases = arrayOfChildren.filter((child) => child.props.when == condition);
```

We also need to create `defaultCases` array which contains child components with no `when` prop defined. In this `filter` function, it selects child components where the `when` prop is falsy or undefined.

Then do the check ensures that there is at most one `<Switch.Default>` component. If there are multiple default cases (i.e., multiple components without a `when` prop), it throws an error to indicate that only one `<Switch.Default>` component is allowed within a `Switch`.

And assign `defaultCase` with either `undefined` or first and only element of  `defaultCases` array.

```jsx
const defaultCases = arrayOfChildren.filter((child) => !child.props.when);
if (defaultCases.length > 1) { 
  throw new Error('Only one <Switch.Default> is allowed');
}
const defaultCase = defaultCases[0];
```

Finally, the last piece of code (provided below) determines what to render based on the `cases` and `defaultCase`. If there are matching cases (i.e., `cases.length > 0`), it returns the components. If there are no matching cases, it returns the `defaultCase` component, or `null` if there's no `defaultCase`.

```jsx
return cases.length > 0 ? <>{cases}</> : <>{defaultCase}</>;
```

## Switch component usage

We are suing `Switch` component inside `App` component. The full code of updated `App` component is below.

```jsx
import { useState } from 'react'
import './App.css'
import { Switch } from './Switch'

function App() {
  const [condition, setCondition] = useState<undefined | string>()

  return (
    <>
      <h3>React Switch Component</h3>

      <div className='field-container'>
        <label className='label' htmlFor="select-input">Possible values:</label>
        <select id="select-input" onChange={(e) => setCondition(e.target?.value)}>
          <option>Not defined</option>
          <option value="1">1</option>
          <option value="2">2</option>
        </select>
      </div>

      <div className='switch-section-container'>
        <div className='label'>Possible values:</div>
        <Switch condition={condition}>
          <Switch.Case when={1}>
            <div className="case-item">1</div>
          </Switch.Case>
          <Switch.Case when={2}>
            <div className="case-item">2</div>
          </Switch.Case>
          <Switch.Default>
            <div className="case-item">Default</div>
          </Switch.Default>
        </Switch>
      </div>
    </>
  )
}

export default App

```

Lets review the changes line by line. 

To demonstrate a usage and test our `Switch` component lets import it to the `App.tsx`

```jsx
import { Switch } from './Switch'
```

Also to change and check conditions we should add some state to the `App` component

```jsx
const [condition, setCondition] = useState<undefined | string>()
```

This state is modified by a select element.

```jsx
<select id="select-input" onChange={(e) => setCondition(e.target?.value)}>
  <option>Not defined</option>
  <option value="1">1</option>
  <option value="2">2</option>
</select>
```

And used to conditionally display different cases content with our newly created `Switch` component.

```jsx
<Switch condition={condition}>
  <Switch.Case when={1}>
	<div className="case-item">1</div>
  </Switch.Case>
  <Switch.Case when={2}>
	<div className="case-item">2</div>
  </Switch.Case>
  <Switch.Default>
	<div className="case-item">Default</div>
  </Switch.Default>
</Switch>
```



## Conclusion
In summary, the Switch component we've created allows a cleaner and more expressive way to handle conditional rendering in React, similar to a switch-case statement. It filters and renders child components based on the provided condition, providing a more intuitive and readable approach to conditional rendering in React applications. While it can be used as-is, it primarily serves as an example of creating conditional rendering components in React.

The complete source code is accessible on GitHub at [this link](https://github.com/musatov/react-switch), and it can be explored interactively using StackBlitz at [this link](https://stackblitz.com/github/musatov/react-switch).

[Mirror of this article](https://musatov.com/posts/react-switch-component) on my personal site.