Jed Watson, JavaScript architect, recently presented at React Conf 2019 and proposed solutions to the design and maintainability challenges posed by components as they inevitably grow to accommodate a large series of use cases. Watson discussed the case of the react-select component, with 2.5M weekly npm downloads, and 100+ options to customize look and feel.
Watson created react-select in October 2014 with limited functionality. Five years later, the project has grown from one GitHub star to 18,000+ stars and 2.5M weekly downloads. Features have been proposed for react-select as varied as search-as-you-type, single- and multi-select, focus management, menu layering and positioning, async menu items, accessibility, keyboard and touch support and creatable items. The growth of features is directly linked to the desire to accommodate an ever-larger set of use cases, as the component was being downloaded and used by more users in an ever greater series of contexts.
The react-select component thus came to feature an intricate and complex logic, as it extended its scope. Watson explained:
Every new use case required more complexity to be added into the core component. […] Everyone who has a different use case for a component like this only needs to customize about 5% but everyone needs to customize a different 5%. So your surface area of what can be customized becomes a significant amount of the component. Because of that, issues and PRs mounted, edge cases conflicted with other edge cases.
Watson came to the realization that to achieve a sustainably maintainable project, he needed to design the component and architect the code in a way that makes it easy for fellow open-source contributors to contribute bug-free, non-conflicting PRs, while enabling users to implement use cases by customization of a few properties.
Watson singled four concerns of the component: state management, functionality, view, and styles. The component user can influence those four concerns through the component interface. In the case of React, this is the React props interface. Additionally, the component user may also use known React compositional patterns such as higher-order components.
State management concerns can be partitioned with higher-order components, tackling a specific piece of state. Watson identified the four key components of state for the select component to be the array of options to select from, the input value, the selected option, and toggles such as menuIsOpen
or isLoading
. In a way consistent with default web patterns, Watson used callbacks to enable customization of the component behavior.
A common pattern involves a higher-order component which implements a callback, which in turn updates a prop reflecting a piece of state. Watson gave the example of a manageState
and makeAsync
higher-order components, which handled input value and loading toggle changes, and which is used as follows:
import BaseSelect from './Select';
import manageState from './manageState';
import makeAsync from './async';
export default manageState(Select);
export const AsyncSelect =
manageState(makeAsync(Select));
Functionality customization and extension are ensured by single-concern props: option label construction, option filtering, option loading, and more. This is in line with the open/closed principle, which prescribes software entities which are closed to modification but open to extension. An example of custom filtering is as follows:
import Select from 'react-select';
filterFn = (candidate, input) =>
candidate.firstName.contains(input) ||
candidate.lastName.contains(input);
<Select filterOption={filterFn} />
View and styles are a function of state. The view concern is split among 25 components (gathered as keys of the components
object prop), which composed together give the displayed react-select component. The 25 components include the views for miscellaneous containers, separators, indicators, the input value, the menu list, and more. By having such atomized, single-concern components, users can customize exactly the part of the view that they need. Each single-concern component can be matched to a style prop which allows component users to customize the appearance of the component. Watson gave the following example, showing a computed style
prop:
options = [
...
];
valueStyles = (styles, {data}) => ({
...styles,
backgroundColor: data.colour,
});
<Select
options={options}
styles={{ value: valueStyles }}
/>
As the following examples showed, user-provided functions or callbacks are extensively used for customization purposes. The functions are passed any relevant data that is internal to the react-select component, such as pieces of state, or default styles. This recalls the dependency inversion principle which states that high-level modules should not depend on low-level modules: both should depend on abstractions. In this case, the abstraction is the prop interface by which the customizing functions are injected into the react-select component. Kent C. Dodds, creator of the popular react-testing-library open-source project, explained in a beginner-friendly article how inversion of control can be leveraged to produce reusable code.
Watson showcased the customizability of the react-select component by implementing a fully functional date picker, through a judicious configuration of props. Watson claimed that reaching this level of architecting and design allowed him to decrease considerably his load as an open-source maintainer. The effort of customizing the component had in effect been offloaded to the component user.
The principles used in the react-select component are not specific to React and generalize to other UI frameworks, though interface and implementation details may change. Frameworks with render functions may use the same interface breakdown techniques. Template-based frameworks may also use higher-order component patterns (sometimes called renderless components in that context). Template-based frameworks will however resort to slot-based composition patterns instead of using render props. Styling concerns may be addressed with CSS variables (supported by most browsers, except IE11) and/or style inlining. A Svelte user provided an example of dynamic styling with CSS properties.
Watson’s full talk is available on ReactConf’s site and contains further code snippets and detailed explanations. React Conf is the official Facebook React event. React Conf was held in 2019 in Henderson, Nevada, on October 24 & 25.