Articles

Avoid soul-crushing components

Kent C. Dodds
Kent C. Dodds

React components afford us the opportunity to encapsulate state, logic, and markup in a single place. A component is like a magical machine where you pass in props and a beautiful component pops out the other end.

Unfortunately, truly maintainable, flexible, simple, and reusable components require more thought than: "I need it to do this differently, so I'll accept a new prop for that." Seasoned React developers know this leads to nothing but pain and frustration for both the maintainer and user of the component. Ever hear of the apropcalypse? It's not fun.

The only thing you can really expect about the future is the unexpected. Nobody can know the future for certain, so you need some tools in your toolbelt that help you avoid committing yourself to a path that may not satisfy the needs of the future. With React components, these tools are patterns that have been battle tested in real applications and libraries and if you're not using them in your components today, you're likely dealing with APIs that result in more workarounds than lines of maintainable code.

And that's slowing you down.

For example

One of my favorite tools for creating great component abstractions is the idea of "compound components." The idea of compound components has been around forever (think <select> and <option>), and it's been implemented in some of my favorite libraries (check Reach UI). Utilizing compound components, you can go from an inflexible API like this:


<Modal
modalTitle="Register"
modalLabelText="Registration form"
openButtonText="Register"
dismissable={true}
contents={
<LoginForm
onSubmit={register}
submitButton={<Button variant="secondary">Register</Button>}
/>
}
/>

To something with ultimate flexibility like this:


<Modal>
<ModalOpenButton>
<Button variant="secondary">Register</Button>
</ModalOpenButton>
<ModalContents aria-label="Registration form">
<ModalDismissButton>
<CircleButton>
<VisuallyHidden>Close</VisuallyHidden>
<span aria-hidden>×</span>
</CircleButton>
</ModalDismissButton>
<ModalTitle>Register</ModalTitle>
<LoginForm
onSubmit={register}
submitButton={<Button variant="secondary">Register</Button>}
/>
</ModalContents>
</Modal>

(Note, the compound components are all the ones that start with Modal).

I can tell some of you are looking at this thinking: "Huh... well I actually like the first one better." But remember, we can't anticipate all the needs of our component's API in the future, so we want to focus on which will be most adaptable.

Consider the difficulty in the following possible future use cases:

  • We want to add a "cancel" button (but still need to support the dismiss button) - We want to style the modal title, open button, or dismiss button (you want to add a openButtonProps prop? lol, I wish I could say I've never tried that before 🤦‍♂️)

Oh, and the neat thing about this is that if you really do like the first one better, there's nothing stopping you from creating a component that uses the compound components API to expose the same API as the first. But try doing that in the reverse 😉

Taking things further, if it's really common enough, you could create a special ModalContents component that handles rendering the dismiss button and title, so in the end it looks like:


<Modal>
<ModalOpenButton>
<Button variant="secondary">Register</Button>
</ModalOpenButton>
<ModalContents aria-label="Registration form" title="Register">
<LoginForm
onSubmit={register}
submitButton={<Button variant="secondary">Register</Button>}
/>
</ModalContents>
</Modal>

But you don't have to sacrifice the benefits of the previous API thanks to composition. Sure you could do something similar with our previous API, but have you ever tried to maintain a component with a hodgepodge of props like that? I mean, I love that React components can be black boxes just as much as the next developer, but I'd prefer to not experience soul-crushing maintainability nightmares any time I need to venture into that black box...

Conclusion

The beauty of patterns like compound components, state reducers, prop getters, controlled props, etc. is that you can have your simple API and a simple implementation at the same time. If you aren't doing this yet, now's a great time to start.

And I'm excited to teach you how on EpicReact.Dev.

Get my free 7-part email course on React!

Delivered straight to your inbox.