If you’ve been watching the React ecosystem the past few years, you’ve surely encountered one of the numerous open source reusable component libraries that developers reach for when building apps. These libraries save us from the tedium of rebuilding the same modals, menus, and form controls over and over for each app we work on.
Those who have used one of these libraries before have probably also run into a case where a component doesn’t quite fit your needs. Maybe the design doesn’t match the mockups your designer provided, and the component doesn’t expose an API for changing these styles. Or maybe you want the behavior of a checkbox but want something that looks completely different.
This is a common sentiment we see at Uber, and leads to wasted effort as teams are often forced to rebuild components from scratch. So when we recently set out to revamp our beloved (but aging) component library, we surveyed dozens of engineers to better understand what roadblocks they run into when reusing existing components. The themes were clear:
- Styles: Developers need to be able to restyle components and their internals. This something that is trivial with global CSS, but has become more difficult in the world of CSS-in-JS where styles are encapsulated within a component and elements have arbitrary classes.
- Props: Sometimes you just need to change the props passed to internal components. For instance adding an
aria-labelto an element, or passing a
classNamethat acts as a target in your integration tests.
- Render: In many cases, people just wanted to be able to completely override the rendering or behavior of certain internals — for example, adding “quick select” options like “Last 30 days” to a datepicker.
We’re certainly not the first to try to tackle these problems. The render props pattern that has been popularized in the React community is one strategy for allowing more control over how a component renders. Paypal’s downshift is a great example of just how far you can take render props.
But while render props are a great tool for many situations, they can be a bit heavy handed if you just want to override a style or change a prop on some internal element. Similarly, component authors sometimes offer props like getFooStyle or getFooProps to customize some internal element, but these are rarely offered in a consistent fashion for all internal elements.
We wanted to come up with a unified API across our components that provides full rendering flexibility, but also has shortcuts for the all-too-frequent occasion that you just need to override some internal style or prop.
The solution we came up with is called the “Overrides” pattern. It’s still very much evolving, but we’ve been impressed with the products developers are building with it so far. We wanted to share it with the broader community in hopes that it either inspires other component library authors or at least raises awareness about the current shortcomings in component reusability. Read on to see how it works.
Overrides Public API
The code snippet below demonstrates what the overrides pattern might look like when customizing a reusable Autocomplete component:
There’s a lot going on here, so let’s walk through some of the key changes:
- Every internal element is given an identifier that developers can target. Here we’re using Root and Option. You can almost think of these like class names (but without the downsides of CSS cascade and global namespace).
- For each internal element, you can override the props, the style, and the component.
- Overriding props is pretty straightforward, the object you specify will be spread in with the default props, taking precedence over them. In this case you can see we’re using it to add an
aria-labelto the root element.
- When overriding style, you can pass either a style object, or a function that receives some props related to the current internal state of the component, allowing you to dynamically change styles based on component state like
isSelected. The style object you return from this function is deep merged with the default element styles.
- When overriding component, you can pass in a stateless functional component or React component class where you supply your own render logic and optionally add other handlers or behavior. This is essentially dependency injection, and unlocks some powerful possibilities.
Offering all of this functionality through a single unified
overrides prop gives developers a consistent place to customize anything they need to.
Overrides in Action
To give you an idea of how our teams are using this, here’s an example from the Uber Freight team:
They wanted to create a form element that shared the same API, keyboard controls, and events as a radio group but with a different visual appearance. They were able to add a sequence of style overrides on top of our existing
RadioGroup component, instead of having to wastefully build, test, and maintain their own custom implementation.
Another example that we used when prototyping this pattern, was adding “Edit” behavior to tags in a multi-select component. In this case we created a component override for
Tag which rendered the existing content but also injected an edit icon.
This illustrates one of the benefits of allowing full components to be injected when compared to render props–you can create new state, lifecycle methods, or even react hooks if you needed to. Our
EditableTag component was able to display a modal when clicked, and then fire off the necessary redux actions to update the tag name.
Overrides Internal Implementation
Here’s how overrides might be implemented internally for our Autocomplete component:
Notice that the render method contains no DOM primitives like
<div>. Instead we import the set of default sub-components from an adjacent file. In this file we use a CSS-in-JS library to create components that encapsulate all the default styles. If a component implementation was passed in through
overrides, it takes precedence over these defaults.
getComponents is just a simple helper function that we use to unpack the overrides and merge them in with the set of default styled components. There are numerous ways to implement this, but here’s the shortest example:
This function assigns style overrides to a
$style prop and merges it in with any other override props–this is because our CSS-in-JS implementation looks for a
$style prop and deep merges it with the default styles.
Each sub-component also receives
sharedProps, which is a set of props related to component state that can be used to dynamically change styles or rendering–for example, changing border-color to red when there’s an error. We prefix these props with
$ as a naming convention to clarify that these are special props which should not be passed down as an attribute to the underlying DOM element.
Trade-offs & Gotchas
As with most software design patterns, there are a few trade-offs to consider when using overrides.
Because each internal element is given an identifier and exposed as a target for overrides, changing the DOM structure could lead to breaking changes more frequently than normal. This goes for CSS changes as well — changing an element from
display: flex to
display: block could theoretically be a breaking change if a consumer was overriding a child element and assumed it was inside of a flexbox. Concerns that would normally be neatly encapsulated inside your component may actually have downstream impacts.
All this means is you’ll likely want to be a bit more careful changing your component DOM structure or styles, and don’t be afraid to do a major version bump when in doubt.
Now that your internal elements are part of your public API, you’ll probably want to write documentation that describes each element and what props it receives. Including a simple diagram of the DOM structure where elements are labeled with their identifier .
Statically typing the overrides object for each component using Typescript or Flow is also a great step forward here, as it will make it clear to developers what props each component will receive and whether the overrides you’re passing in are compatible.
Imagine you’re building a reusable
Pagination component which uses a
Button internally, how do we expose the
Button overrides via
Pagination? And what if there are multiple buttons (First, Prev, Next, Last, etc) that the consumer may want to style differently? We have some ideas on how to approach this problem, but there’s no silver bullet and it will likely require some experimentation before the right solution emerges.
Supporting overrides for your components adds complexity and forces you to think more critically about how consumers will override your internals. If you’re just building a component that will be reused a couple times within your own app, this added complexity is likely not worth it. But if you’re building a reusable component library that will be used by hundreds of engineers, sacrificing simplicity for the benefit of your consumers makes a lot of sense. For us it was an easy decision, and we’ve been consistently impressed with the clever use cases engineers are coming up with.
If you want to see how we’re using this pattern in our own component library, you can browse the documentation or source code. Some awesome folks also created their own overrides implementation after reading this post — you may find their projects helpful: (1) tlrobinson/overrides / (2) react-overrides.
Hopefully you find this pattern useful, or at the very least have some new ideas on how to make your React components more reusable!
Thoughts or questions? Leave a comment or tweet me: @dschnr