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:
aria-label
to an element or need to pass a className
as a target for your tests.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.
Take a look at the following code snippet as it depicts how to customize a reusable autocomplete component with the overrides pattern:
1// App.js
2render() {
3 <Autocomplete
4 options={this.props.products}
5 overrides={{
6 Root: {
7 props: {'aria-label': 'Select an option'},
8 style: ({$isOpen}) => ({borderColor: $isOpen ? 'blue' : 'grey'}),
9 },
10 Option: {
11 component: CustomOption
12 }
13 }}
14 />
15}
16
17// CustomOption.js
18export default function CustomOption({onClick, $option}) {
19 return (
20 <div onClick={onClick}>
21 <h4>{$option.name}</h4>
22 <small>{$option.price}</small>
23 </div>
24 );
25}
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.
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:
1 // Autocomplete.js
2import React from 'react';
3import * as defaultComponents from './styled-elements';
4
5class Autocomplete extends React.Component {
6
7 // Setup & handlers omitted to keep this example short
8
9 getSharedStyleProps() {
10 const {isOpen, isError} = this.state;
11 return {
12 $isOpen: isOpen
13 $isError: isError
14 };
15 }
16
17 render() {
18 const {isOpen, query, value} = this.state;
19 const {options, overrides} = this.props;
20
21 const {
22 Root: {component: Root, props: rootProps},
23 Input: {component: Input, props: inputProps},
24 List: {component: List, props: listProps},
25 Option: {component: Option, props: optionProps},
26 } = getComponents(defaultComponents, overrides);
27
28 const sharedProps = this.getSharedStyleProps();
29
30 return (
31 <Root {...sharedProps} {...rootProps}>
32 <Input
33 type="text"
34 value={query}
35 onChange={this.onInputChange}
36 {...sharedProps}
37 {...inputProps}
38 />
39 {isOpen && (
40 <List {...sharedProps} {...listProps}>
41 {options.map(option => {
42 <Option
43 onClick={this.onOptionClick.bind(this, option)}
44 $option={option}
45 {...sharedProps}
46 {...optionProps}
47 >
48 {option.label}
49 </Option>
50 })}
51 </List>
52 )}
53 </Root>
54 );
55 }
56}
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:
1function getComponents(defaultComponents, overrides) {
2 return Object.keys(defaultComponents).reduce((acc, name) => {
3 const override = overrides[name] || {};
4 acc[name] = {
5 component: override.component || defaultComponents[name],
6 props: {$style: override.style, ...override.props},
7 };
8 return acc;
9 }, {});
10}
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.
As always with software design patterns, there is a compromise and a few trade-offs to ponder before using overrides.
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.
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.
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.
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.
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.