Limit and require props in React components
Tags: webdev, tutorial, react
Published: 2023-05-22 05:51:16

> Also available on my [blog](https://nikolasbarwicki.com/articles/limit-require-props-components-based-on-other-props)

## Introduction

As a frontend developer, it's essential to create **foolproof components** in order to maintain a clean and robust codebase. One way to achieve this is by effectively **limiting** and **requiring** prop combinations in your components.

Prop combinations enable developers to create **reusable** components with a well-defined interface. Limiting and requiring certain combinations makes it easier to debug and maintain the code, preventing potential bugs caused by conflicting or unexpected combinations of properties. This article will guide you through the process of limiting and requiring prop combinations in React using an example component. We will discuss the concepts of function overloads, mirroring an HTML element, and protecting input with types.

### Importance of creating foolproof components

In software development, especially when working with a team, it's crucial to create components that are clear in their intention, easy to use, and composable. Foolproof components reduce the possibility of developers misusing them, which can lead to bugs and hard-to-maintain code. Limiting and requiring prop combinations is a step towards creating high-quality, foolproof components with a clear purpose and API.

### The need for limiting and requiring prop combinations

By explicitly defining which prop combinations are allowed in your components, you can **simplify the logic** for handling props and **avoid possible bugs** caused by incorrect usage. Moreover, this makes it simpler for other developers to understand how to use your component correctly and sets up the codebase for **long-term maintainability**.

Now let's dive into some in-depth examples and discuss how to approach limiting and requiring prop combinations.

## Requiring prop combinations: The Toggle Component

Let's consider a simple `<Toggle />` component as an example. This component will have two states: **_ON_** and **_OFF_**. When the `status` prop is provided, it should also require an `onChange` prop, a callback function that updates the state.

1. Begin by defining the prop types for the `<Toggle />` component:

```typescript
type ToggleProps = {
  status?: boolean
  onChange?: (newStatus: boolean) => void
}
```

2. Next, create two prop combinations: one without the `status` and `onChange` props, and another that requires both `status` and `onChange`.

```typescript
type ControlledToggleProps = ToggleProps & {
  status: boolean
  onChange: (newStatus: boolean) => void
}

type UncontrolledToggleProps = ToggleProps & {
  status?: never
  onChange?: never
}
```

3. Implement [function overloads](https://nikolasbarwicki.com/articles/mastering-function-overloading-in-typescript) in the `<Toggle />` component to handle the two prop combinations:

```tsx
function Toggle(props: ControlledToggleProps): JSX.Element
function Toggle(props: UncontrolledToggleProps): JSX.Element
function Toggle({
  status = false,
  onChange,
}: ControlledToggleProps | UncontrolledToggleProps) {
  const [internalStatus, setInternalStatus] = useState(status)

  function handleClick() {
    const newStatus = !internalStatus
    setInternalStatus(newStatus)

    if (onChange) {
      onChange(newStatus)
    }
  }

  return (
    <div onClick={handleClick}>
      {internalStatus ? 'Switch is ON' : 'Switch is OFF'}
    </div>
  )
}
```

## Limiting prop combinations: The Alert Component

For our next example, let's say we have an `<Alert />` component that displays different types of alert messages: `info`, `success`, and `error`. We want to limit the prop combinations to allow only one of these types to be passed to the component simultaneously.

1. Define the prop types for the `<Alert />` component:

```typescript
type AlertProps = {
  children: React.ReactNode
  type?: 'info' | 'success' | 'error'
}
```

2. Next, create three prop combinations for the different types:

```typescript
type InfoAlertProps = AlertProps & { type: 'info' }
type SuccessAlertProps = AlertProps & { type: 'success' }
type ErrorAlertProps = AlertProps & { type: 'error' }
```

3. Implement function overloads in the `<Alert />` component for these prop types:

```tsx
function Alert(props: InfoAlertProps): JSX.Element
function Alert(props: SuccessAlertProps): JSX.Element
function Alert(props: ErrorAlertProps): JSX.Element
function Alert({ children, type = 'info' }: AlertProps) {
  const alertClass = `alert alert-${type}`

  return <div className={alertClass}>{children}</div>
}
```

By following these examples, you can structure your components, prop types, and function overloads to allow certain prop combinations while disallowing others. This approach not only **simplifies** your code and **prevents bugs** but also promotes **better collaboration** and **maintainability** in your React projects.

## Mirroring to an HTML element: The Tooltip Component

In some cases, it is necessary to allow our custom components to accept additional attributes that may be passed along to an HTML element. For instance, imagine a `<Tooltip />` component that provides additional information when hovering over an element. While it should accept custom props to handle the tooltip itself, it should also allow the user to set any other attributes for the underlying HTML element.

### Problems with the Tooltip component

Let's say we have the following `<Tooltip />` component:

```tsx
type TooltipProps = {
  text: string
  position?: 'top' | 'bottom' | 'left' | 'right'
}

function Tooltip({ text, position = 'top', ...otherProps }: TooltipProps) {
  // ...tooltip logic...
  return (
    <div className={`tooltip tooltip-${position}`} {...otherProps}>
      {text}
    </div>
  )
}
```

The issue with this component is that while we can pass the `text` and `position` props, we cannot pass any other props to the underlying `div` that might be useful or necessary, such as `className`, `style`, or `id`.

### The solution: Extending React.ComponentPropsWithoutRef

To resolve this issue, we can use the `React.ComponentPropsWithoutRef` type to merge our custom props with the built-in props for the HTML element:

```typescript
type TooltipProps = {
  text: string
  position?: 'top' | 'bottom' | 'left' | 'right'
} & React.ComponentPropsWithoutRef<'div'>
```

Now the `<Tooltip />` component can accept any prop that a native `div` component would accept:

```tsx
<Tooltip
  text="Hello, world!"
  position="bottom"
  className="custom-tooltip"
  style={{ backgroundColor: 'red' }}
  id="tooltip"
>
  Hover me
</Tooltip>
```

The final `<Tooltip />` component with this enhancement:

```tsx
type TooltipProps = {
  text: string
  position?: 'top' | 'bottom' | 'left' | 'right'
} & React.ComponentPropsWithoutRef<'div'>

function Tooltip({ text, position = 'top', ...otherProps }: TooltipProps) {
  // ...tooltip logic...
  return (
    <div className={`tooltip tooltip-${position}`} {...otherProps}>
      {text}
    </div>
  )
}
```

## Summary

Throughout this article, we've explored different ways of **limiting** and **requiring** prop combinations in React components by using function overloads and the never type. We've also discussed how to mirror an HTML element's props to provide a more inclusive and flexible API for custom components.

Limiting and requiring prop combinations enable developers to create cleaner, more maintainable code by reducing the likelihood of bugs and misunderstandings. By leveraging the power of [Typescript](https://nikolasbarwicki.com/categories/typescript) with [React](https://nikolasbarwicki.com/categories/react), frontend developers can create foolproof components, making it easier to work with complex applications and collaborate with fellow developers.

## Conclusions

Continuous learning and improvement are essential aspects of being a frontend developer. Understanding the importance of creating **foolproof**, **efficient**, and **reusable** components is **vital in this ever-evolving industry**. By mastering the concepts discussed in this article and applying them to your projects, you'll elevate your skills and contribute to more effective code in your projects.