skip to content
Nikolas Barwicki - Javascript Blog Nikolas's Blog

Limit and require props in components based on other props

/ 6 min read

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:
type ToggleProps = {
  status?: boolean
  onChange?: (newStatus: boolean) => void
}
  1. Next, create two prop combinations: one without the status and onChange props, and another that requires both status and onChange.
type ControlledToggleProps = ToggleProps & {
  status: boolean
  onChange: (newStatus: boolean) => void
}

type UncontrolledToggleProps = ToggleProps & {
  status?: never
  onChange?: never
}
  1. Implement function overloads in the <Toggle /> component to handle the two prop combinations:
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:
type AlertProps = {
  children: React.ReactNode
  type?: 'info' | 'success' | 'error'
}
  1. Next, create three prop combinations for the different types:
type InfoAlertProps = AlertProps & { type: 'info' }
type SuccessAlertProps = AlertProps & { type: 'success' }
type ErrorAlertProps = AlertProps & { type: 'error' }
  1. Implement function overloads in the <Alert /> component for these prop types:
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:

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:

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:

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

The final <Tooltip /> component with this enhancement:

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