DEV Community

Lars-Erik Bruce
Lars-Erik Bruce

Posted on

5 killer tricks to make your React Design System Component highly reusable! (You won't believe the Bonus trick!)

You have just made your perfect Component for the company's design system, and are ready to ship it out in your Component Library. But, are the developers depending on it as excited as you? No, because they know, with their new Component follows restraints in the form of a straight jacket and a trip to the nearest mental hospital!

Why? Because the Jira asked for a rainbow component with custom colors, and odd behavior, which makes perfect business sense from their point of view, but that you "forgot" to think about in your crazy laboratory while cooking up new components!

Don't fret! Let me tell you what features you should always include, so developers won't send you ugly thoughts and karma while they are crying themselves into sleep at night.

Lets start with a made-up component. Scenario: You want to style a FormComponent in accordance with the design system sketches. Let's say you came up with the following:

export function FormComponent({ prefix, value, onChange }) {
  return (
    <div className={css.fsComponent}>
      <div className={css.fsComponentHeader}>{prefix}</div>
      <input
        className={css.fsComponentInput}
        type="text"
        value={value}
        onChange={onChange}
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

It does exactly what is asked for, a simple input element with a prefix (and of course, your beautiful CSS styling). But how can we make this component both easier and more flexible to use?

1. Utilize PropTypes or TypeScript for type safety!

Before we do anything else, we should add types to our component, so that developers using it gets a clear idea how it can be used automatically through their code editor:

type Props = {
  prefix: string;
  value: string;
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
};

export function FormComponent({ prefix, value, onChange }: Props) {
  return (
    <div className={css.fsComponent}>
      <div className={css.fsComponentHeader}>{prefix}</div>
      <input
        className={css.fsComponentInput}
        type="text"
        value={value}
        onChange={onChange}
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

But, it looks like we adopted the mentality of restraining our developers in our types! What if the Jira calls for a flag icon in the input prefix?!

2. Use React.ReactNode as type for children and other props for jsx-arguments

Don't impose strict rules about what is allowed for the compositional props, like children (or prefix in our example) . We never know how our Components are going to be used in advance.

If we want to render props straight into the JSX-tree, we should never enforce the developers to send a string. Or an element for that sake! Sometimes, I just want to use a string as a child, but I have to put it inside a span, because the component requires an element! :-(

type Props = {
  prefix: React.ReactNode;
  value: string;
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
};
Enter fullscreen mode Exit fullscreen mode

React.ReactNode allows for null, undefined, string, number and all variants of React elements!

3. Always include all props from the proper DOM-element

Never limit the users of your component for what already exists in the DOM API! This not only allows the users of the component to customize your component with style or className when needed (which I'm sure you dread), it also gives them access to literally hundreds of other attributes from the DOM API!

type Props = {
  prefix: React.ReactNode;
  value: string;
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
} & React.HTMLAttributes<HTMLDivElement>;

export function FormComponent({ prefix, value, onChange, ...props }: Props) {
  return (
    <div {...props} className={css.fsComponent + ' ' + (props.className || '')}>
      ...
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

You can use the React.HTMLAttributes to easily add all DOM attributes as properties to your component. Remember to send in the HTMLElement that you use as a wrapper element (HTMLDivElement if it is a div, etc).

4. Propagate ref with forwardRef

If you include a way for the developer to refer to your component, they are allowed to do basic stuff like managing focus, measure dimensions and positions and even integrate with third party DOM libraries!

type Props = {
  prefix: React.ReactNode;
  value: string;
  ref?: React.ForwardedRef<HTMLDivElement>;
  inputRef?: React.ForwardedRef<HTMLInputElement>;
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
} & React.HTMLAttributes<HTMLDivElement>;

export function FormComponent({
  prefix,
  value,
  onChange,
  ref,
  inputRef,
  ...props
}: Props) {
  return (
    <div
      {...props}
      className={css.fsComponent + ' ' + (props.className || '')}
      ref={ref}
    >
      <div className={css.fsComponentHeader}>{prefix}</div>
      <input
        ref={inputRef}
        className={css.fsComponentInput}
        type="text"
        value={value}
        onChange={onChange}
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

You don't have to provide a ref for every HTML tag you use in the components JSX. But the container element should have one, and also some pivotal elements, like input elements, could benefit from this.

This way, the developers using your component can speak directly with the elements DOM API with ease, when needed.

5. Support both controlled and uncontrolled modes

Don't decide how developers should use your component. Support both controlled and uncontrolled modes. Right now, we have a controlled component. By turning it into a component that can be used both controlled and uncontrolled, we empower the users of our Component.

export function FormComponent({
  prefix,
  value,
  ref,
  onChange,
  inputRef,
  ...props
}: Props) {
  const [internalValue, setInternalValue] = useState(value);

  useEffect(() => {
    // Update value if it is used as a controlled component
    if (onChange === undefined) {
      setInternalValue(value);
    }
  }, [value, onChange]);

  return (
    <div
      {...props}
      className={css.fsComponent + ' ' + (props.className || '')}
      ref={ref}
    >
      <div className={css.fsComponentHeader}>{prefix}</div>
      <input
        ref={inputRef}
        className={css.fsComponentInput}
        type="text"
        value={onChange === undefined ? internalValue : value}
        onChange={(e) => {
          if (onChange) {
            onChange(e);
          } else {
            setInternalValue(e.target.value);
          }
        }}
      />
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

Bonus 6! Keep functionality out of your design style library!

If your component library is meant to enforce a style guide, or a design pattern, why not let the users of the component library worry about the logic and only implement the styling? Then you also have a lot less of bug testing to worry about (I'm certain you will find bugs in the above example, for instance). What about just this:


type Props = {
  prefix: React.ReactNode;
  children: React.ReactNode;
  ref?: React.ForwardedRef<HTMLDivElement>;
} & React.HTMLAttributes<HTMLDivElement>;

export function FormComponent({ prefix, children, ref, ...props }: Props) {
  return (
    <div
      {...props}
      className={css.fsComponent + ' ' + (props.className || '')}
      ref={ref}
    >
      <div className={css.fsComponentHeader}>{prefix}</div>
      <div className={css.fsComponentInputContainer}>{children}</div>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

Style it up assuming that {children} is an input-element, state this clearly in the documentation, and let the developers deal with it if they feel they need something else. Then the design system only deals with the looks and feel of the application, while the application code deals with the functionality:

<FormElement prefix={<Icon svgPath={keyIconPath} />}>
  <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
</FormElement>
Enter fullscreen mode Exit fullscreen mode

Top comments (0)