Writing reusable React components
In the past we used to write our React components in a particular way which did not encourage component reuse. In this article I’ll show our past approach, explain how and why it makes reuse difficult, and outline my new way of writing React components that I’ll try in my upcoming projects.
I’ll use a pretty simple Button component for illustration. In the past we’d design the component along these lines:
interface ButtonProps {
inverted?: boolean
disabled?: boolean;
onClick: () => void;
}export function Button(props: ButtonProps) {
return <button … />
}
Note that the interface lists only very few properties. They an can be categorised into:
- Properties which are used for internal purposes, to describe custom behaviour or styling (`inverted`)
- Properties which are forwarded to the underlying HTML element (`disabled`, `onClick`)
The underlying HTML element (`<button>`) supports many more properties which we didn’t include. Also, while our definition of the `onClick` prop is technically compatible with the `onClick` prop of a `<button>`, it doesn’t include the event argument, making the function less useful in some situations.
We excluded the underlying `<button>` props for two reasons:
- We didn’t need them in the particular project. HTML elements have a lot of props, including all of them in the interface would be wasted work.
- We explicitly didn’t want to include certain properties (eg. `style`) to encourage best practices (eg. use CSS instead of inline styles).
The first point is due to laziness, and can be solved with better tools. Or rather, with better understanding of our existing tools! The React type definitions come with types which describe the props of all HTML elements, and we should make use of it:
interface ButtonProps extends React.ComponentProps<”button”> {
inverted?: boolean;
}
With this definition, the component accepts all the same props that `<button>` does, plus our own. We just need to make sure to forward all the extra props to the underlying HTML element.
export function Button({ inverted, …props }: ButtonProps) {
return <button {…props} />
}
For the majority of the components we write, we can identify what the underlying HTML element will be. Very often it’s a `<div>`, but it might also be a `<button>`, or `<input>` etc. Our components should be explicit about what the root element is and accept all their properties.
The second reason to exclude properties is I believe misguided. The type system is not the right place to enforce best practices. Best practices often stand in the way of experiments and hinder playful exploration.
For the same reason that I sometimes create Git commits with messages such as “…” or “WIP” , I want to be able to write all the dirty ugly code that I need in order to explore ideas. Yes, I know it’s bad code, but trust me, I’ll clean it up before pushing it to GitHub.
Furthermore, excluding specific props makes it seem that code which uses these is bad and everything else is good. But the situation is not black and white like a strict type system makes it appear. Just because you’re /not/ using inline styles doesn’t mean you’re doing it right, there are still plenty of ways to write bad code without inline styles.
Lastly, if it works it ain’t stupid.
Linters (~automated tools) and code reviews (~humans) are much better places to enforce best practices. If only for the simple reason that you can disable them, or discuss the code with your peers if you really feel the rules shouldn’t apply in your particular situation.
I haven’t talked about refs yet, but it’s important that any component which renders a specific HTML element should also accept ref and forward it to the element.
How does this look in practice? When creating a new component go through these steps:
- Decide what the underlying HTML element is.
- Write down the interface, extending `React.ComponentPropsWithRef<…>`.
- Define the component with `React.forwardRef()`, and forward the ref as well as any extra props to the underlying HTML element.
interface ButtonProps extends React.ComponentPropsWithRef<”button”> {
inverted?: boolean;
}export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button({ inverted, …props }, ref) {
return <button ref={ref} {…props} />
});
Sometimes you may want to change the underlying HTML element. This is often done for semantic reasons. A good example here is if you want to have a link (`<a>`), that looks like a button. Material-UI does it rather well, all of their components have a uniform API to do that:
import { Button } from “@material-ui/core”<Button component=”a” href=”/home”>Click Me!</Button>
Supporting this is not straightforward in the type system. The type of underlying HTML element changes the types of props that the component accepts. When you use `<Button>` it should accept all `<button>` props but when you use `<Button component=“a”`> it should accept all `<a>` props (eg. `href`).
How to express that nicely in the type system is material for another article though. Until then, happy coding.