Antipattern: Overflexible Props in React/TypeScript

Introduction

In the world of React development, utilizing TypeScript offers numerous advantages, including strong typing and enhanced code readability. However, certain practices can undermine these benefits, leading to what is known as an "antipattern." One such antipattern emerges when developers allow a React component to accept any properties by leveraging TypeScript's Record<string, unknown>, a type that allows for an object to have any number of properties of unknown type. This approach can cause significant confusion and maintainability issues in large codebases.

Understanding the Antipattern

Consider the following React component, which uses TypeScript:

import React from 'react';

export const CustomInput = (props: {
  name: string;
  label: string;
} & Record<string, unknown>) => {
  const { name, label, ...otherprops } = props;

  return (
    <div>
      <label htmlFor={name}>First name:</label>
      <input id={name} name={label} {...otherprops} />
    </div>
  );
};

This component defines its props as a combination of specific properties (name and label) and an open-ended record that can accept any additional properties. The issue here is that while this grants flexibility, it also allows developers to pass irrelevant properties that the component does not actually use or need. Such properties can clutter the component's interface, making it difficult for other developers to understand what props are essential and what each prop does.

For example, if a developer mistakenly passes a backgroundColor property to the CustomInput component, which does not handle styling, this property will be passed down to the <input> element, potentially leading to unexpected results and a difficult-to-track source of errors.

Why Use This Antipattern?

Developers might opt for this pattern for a few reasons:

  • Flexibility: By allowing any prop, developers can avoid repeatedly updating the component's prop types when new props need to be passed to underlying HTML elements or when the component is used in various contexts.
  • Convenience: It saves time in the short run, especially when the component needs to pass through many props to child components or HTML elements without explicitly defining each one.

Better Approaches

Using ComponentProps for HTML Elements

To ensure that only relevant and supported props are passed to components, it is better to use the ComponentProps utility from React. ComponentProps is a TypeScript utility type that extracts the prop types from a component or an HTML element, ensuring that developers only pass props that are appropriate. This approach restricts the props to those actually used by the component, as shown in the improved version of the CustomInput component:

import React, { ComponentProps } from 'react';

export const CustomInputTyped = (props: {
  name: string;
  label: string;
} & ComponentProps<'input'>) => {
  const { name, label, ...otherprops } = props;

  return (
    <div>
      <label htmlFor={name}>First name:</label>
      <input id={name} name={label} {...otherprops} />
    </div>
  );
};

Using ComponentProps with Custom Components

Similarly, when creating wrappers for custom components, it's advisable to use ComponentProps with the type of the component you are wrapping. This ensures that the wrapper only accepts props that are appropriate for the wrapped component:

import React, { ComponentProps } from 'react';
import { CustomInputTyped } from './CustomInputTyped';

export const CustomInputTypedWrapper = (props: {
  zIndex: number;
} & ComponentProps<typeof CustomInputTyped>) => {
  const { zIndex, ...otherprops } = props;

  return (
    <div style={{ zIndex }}>
      <CustomInputTyped {...otherprops} />
    </div>
  );
};

Conclusion

While the flexibility to pass any props to a React component can seem appealing, it often leads to confusing and cluttered components. By using TypeScript's capabilities more judiciously, such as through ComponentProps, developers can maintain clarity and enforce better prop management, leading to cleaner, more maintainable code. This approach not only harnesses the full power of TypeScript with React but also ensures that components remain predictable and their interfaces clear. Review your own projects to identify if you might be inadvertently using this antipattern and consider refactoring for better code health.