Building atomic reusable React components

If you ever worked on big React projects, you certaintly had to write smaller components to be reused by bigger ones. In this case I am calling these components atoms. If you want to know more about Atomic Design, you can check this page by Brad Frost which explains it very well: https://bradfrost.com/blog/post/atomic-web-design/

Writting a simple input element

The basic principles for writting scalable components are the following

1. Localize your styles

In the example posted bellow, I have used CSS modules that provide local scoping by default. Another way to localize styles, is to use CSS scoping and good methodologies like BEM and SMACSS to avoid selector specificity and make your CSS scalable.

Example:

.card {
  &__header { ... }
  &__body { ... }
  &__footer { ... }

  .is-active { ... }
}

2. All data should be passed through props

Reusable atomic components should not be responsible for any data manipulation / fetching / state. The state should be only used for UI / UX. This will minimize the component dependencies and make it testable with simple assertions.

3. Props should extend native HTML element props

Make sure you extend the root element native props, so that we don't restrict consumers from using whatever props they need (unless it's a requirement). In this example, I am moving the className to the wrapper element so that consumers can have more flexibility if they need to overwrite styles (they shouldn't, but ok…). The remaining props are passed to the native element.

type InputProps = {
  iconUrl?: string;
} & React.InputHTMLAttributes<HTMLInputElement>;

const Input = ({ iconUrl, className, ...nativeProps }: InputProps) => {
  const rootElementClassname = "input";
  const classes = {
    root: cn(
      styles[rootElementClassname],
      {
        [styles["has-icon"]]: iconUrl
      },
      className
    ),
    icon: styles[`${rootElementClassname}__icon`],
    nativeElement: styles[`${rootElementClassname}__native-element`]
  };

  return (
    <div className={classes.root}>
      {iconUrl && <img src={iconUrl} alt="icon" className={classes.icon} />}
      <input {...nativeProps} className={classes.nativeElement} />
    </div>
  );
};

You can check the full example in my codesandbox.