How to Override Existing React Components
Jun 15, 2020 • 10 Minute Read
Introduction
Many developers turn to open-source reusable component libraries when building apps. These libraries save time and effort and minimize the tedious work of frequently rebuilding much-used components such as menus, form controls, and modals.
However, even if you use such libraries, you may wish to alter the behavior or appearance of a component. There are indeed cases where the design may not match the mock-ups provided by the designer, and the component does not offer a method to change its styles.
Luckily, there is a solution. Read this guide to learn how to override existing React.js class.
Before you dive in, it is important to look at the big picture and understand the factors that frequently hinder the straightforward employment of existing components:
- Styles: You may need to restyle components along with their internals. This is rather trivial with global CSS. However, it can be a bit tricky when it comes to CSS-in-JS since components actually encapsulate styles.
- Props: You may need to alter what props are passed to the internal components. Assume, for example, that you want to add an aria-label to an element or need to pass a className as a target for your tests.
- Render: You may simply wish to override the rendering or behavior of particular internals. For instance, you may want to add clever select options such as “Last 30 days” to a datepicker.
One solution that you may find helpful if you wish to have more control over how a component renders is the render props pattern. However, render props can be a bit of a drag if you only wish to override a style or modify a prop on an internal element. Furthermore, some developers provide props such as getFooStyle or getFooProps to customize the inner elements, but such gestures are rarely consistent and never guaranteed.
Thus, the perfect solution ought to be a unified API across all components that is adaptable, flexible, and easy to use. This solution is the overrides pattern.
Overrides Public API
Take a look at the following code snippet as it depicts how to customize a reusable autocomplete component with the overrides pattern:
// App.js
render() {
<Autocomplete
options={this.props.products}
overrides={{
Root: {
props: {'aria-label': 'Select an option'},
style: ({$isOpen}) => ({borderColor: $isOpen ? 'blue' : 'grey'}),
},
Option: {
component: CustomOption
}
}}
/>
}
// CustomOption.js
export default function CustomOption({onClick, $option}) {
return (
<div onClick={onClick}>
<h4>{$option.name}</h4>
<small>{$option.price}</small>
</div>
);
}
Each element now has an identifier that developers can single out. The above code uses Root and Option. It is easier to think of these identifiers as class names minus the hassle of CSS cascade and global namespace.
You can override the props, the style, and the component for each and every internal element. The overriding process is quite easy. Once you designate an object, it is propagated along the default props with a higher priority. As you see in the above code, it is used to append an aria-label to the root element.
To override a style, you have two options. Either you can pass a style object, or you can pass a function that will obtain props regarding the current internal state of the component, thus enabling you to modify styles dynamically depending on the component state, such as isError or isSelected. Keep in mind that the style object returned from the function is now integrated with the default element styles.
When you override a component, you have the option to pass in a stateless functional component or React component class and later provide your own rendering behavior, or better yet, add different handlers. Think of it as a form of dependency injection that can unleash endless possibilities.
Overrides in Action
To understand how developers use overrides, take a look at this example. The goal is to produce a form element that has the same API, keyboard controls, and events as a radio group but is visually different. The solution is to add a sequence of style overrides on top of the already functional RadioGroup component. This saves time, effort, and cost in development and maintenance.
The following code demonstrates how to implement overrides internally for an autocomplete component:
// Autocomplete.js
import React from 'react';
import * as defaultComponents from './styled-elements';
class Autocomplete extends React.Component {
// Setup & handlers omitted to keep this example short
getSharedStyleProps() {
const {isOpen, isError} = this.state;
return {
$isOpen: isOpen
$isError: isError
};
}
render() {
const {isOpen, query, value} = this.state;
const {options, overrides} = this.props;
const {
Root: {component: Root, props: rootProps},
Input: {component: Input, props: inputProps},
List: {component: List, props: listProps},
Option: {component: Option, props: optionProps},
} = getComponents(defaultComponents, overrides);
const sharedProps = this.getSharedStyleProps();
return (
<Root {...sharedProps} {...rootProps}>
<Input
type="text"
value={query}
onChange={this.onInputChange}
{...sharedProps}
{...inputProps}
/>
{isOpen && (
<List {...sharedProps} {...listProps}>
{options.map(option => {
<Option
onClick={this.onOptionClick.bind(this, option)}
$option={option}
{...sharedProps}
{...optionProps}
>
{option.label}
</Option>
})}
</List>
)}
</Root>
);
}
}
As you see, the render method does not include DOM primitives such as <div>. Alternately, you must import the default sub-component set from an adjacent file. The above example uses a CSS-in-JS library to generate components that encompass all the default styles. Thus, whenever a component's implementation is passed using overrides, it prevails over the defaults.
Note that getComponents is a mere helper function used to unload the overrides and combine them within the set of default styled components. The simplest approach to achieve this is shown below:
function getComponents(defaultComponents, overrides) {
return Object.keys(defaultComponents).reduce((acc, name) => {
const override = overrides[name] || {};
acc[name] = {
component: override.component || defaultComponents[name],
props: {$style: override.style, ...override.props},
};
return acc;
}, {});
}
This style overrides to a $style prop and combines it with all override props. This is due to the fact that the original CSS-in-JS implementation detects the $style prop and deep merges it along the default styles.
The sub-components also receive sharedProps. The latter is a set of props concerning the component state, and these props are used to alter the styles or rendering of the component dynamically. For instance, the border color may change to red in case of an error. Such props are prefixed with $ to indicate that these are distinctive props and are not to be passed as an attribute to the underlying DOM element.
Trade-offs
As always with software design patterns, there is a compromise and a few trade-offs to ponder before using overrides.
Rigidity
Since each internal element now has a unique identifier allowing it to be a target for overrides, modifying the DOM structure may frequently result in breaking changes. This issue also applies for changes in CSS. For instance, if the consumer tries to override a child element, unwittingly assuming it was within a flexbox, then changing from display: flex to display: block may prove to be a breaking change.
Similar considerations that otherwise would be easily dismissed, encapsulated within your component, are now a crucial issue.
For this reason, you must be very careful when you modify your component DOM structure or styles. When in doubt, opt for a major version bump.
Documentation
With overrides, your internal elements are now of the public API. If you want to be thorough, provide a documentation that explains each element and what props it receives. You may also consider adding a friendly diagram of the DOM structure and label elements with their identifiers.
To make the lives of other developers easier, you may opt to type the overrides object for each component using Typescript or Flow.
Composition
Assume that you wish to build a reusable pagination component that uses a button internally. You have to consider several factors for this one, simple notion. How do you plan on exposing the button overrides via pagination? Is there more than one button that the consumer may wish to style? You may have to experiment to find the method that suits your app.
Complexity
It is a given that overrides add complexity to the job. You have to consider all the ways that a consumer will override your internals. For a simple component that you will reuse a couple of times within your own app, you are better off without the extra complexity. However, if your app will be used by many developers, then complexity is a price that you must pay.
Conclusion
This guide covered overrides and provided a simple demonstration of how to use them. Overrides are a fresh concept that is still evolving. However, the results of using it are quite impressive. It provides developers with consistent methods to customize whatever they need to using a unified API.