-
Notifications
You must be signed in to change notification settings - Fork 4
Component Design
Reapit Elements aims to be an implementation of the Reapit Design System, and since the Design System will evolve over time, so to will Elements. In order to support this evolution, we need to maintain a good balance between consistency and flexibility, so that:
- Elements and its consumers have the flexibility to grow with the Design System as it evolves;
- Elements helps consumers achieve UI and UX outcomes that are consistent with the Design System.
To achieve the right balance, we need to take care when defining the API for a component.
- A component with an overly flexible API will typically allow for inconsistent usage and outcomes across consumers, but often allows evolution to occur more cheaply or quickly. Flexibility can also help different consumers integrate the Design System's components with their own technology choices.
- A component with an overly constrained API will typically allow for very consistent outcomes across consumers, but may not permit much evolution to occur without significant cost. Constrained component APIs can make the delivery of common patterns amongst different consumers using the same technology simpler to achieve.
Understanding these two dimensions in the context of a Design System component library will help you better contribute to the design system itself, as well as write code for day-to-day product features that is more easily shared with other features in future.
Our goal is to design simple, composable components that help provide consistency and speed of delivery across Reapit's web applications. In order to do that, here’s a set of principles that help us always be on that path.
- Simplicity: Strive to keep the component API fairly simple and show real use-cases for the component in the component’s story/documentation. Always prefer the composition of atomic components over configuration of a monolith.
- Composition: Break down components into smaller parts with minimal props to keep complexity low, and compose them together. This will ensure that the styles and functionality are flexible and extensible. The common design patterns described below should be considered when deciding how best to break down a large component or compose smaller ones.
- Naming Props: We all know naming is the hardest thing in this industry. Generally, ensure a prop name is indicative of what it does and avoid coupling it to a specific UI element, that way, the UI representation of the component can change, without the component’s interface needing to as well.
The following patterns are all quite common throughout the React community. Each of the patterns are demonstrated through the implementation of a (slightly contrived) hypothetical Toggle component comprised of an “on” label, an “off” label, and a button. The examples here closely follow those used by Kent C. Dodds in Advanced React Component Patterns; it’s worth viewing the course as it covers more ground that the following content does (just be aware that the course is quite old and deals exclusively with class components rather than function components). Alternatively, Lydia Halley covers some of the same React-specific patterns alongside other generic JavaScript patterns in A Tour of JavaScript & React Patterns.
There are cases where a number of components will be closely (or even directly) coupled to each other. Often, it can be appropriate to encapsulate all of these components in a single interface; for example, a form field component may adequately encapsulate the use of label, input and validation message components without unnecessarily constraining consumers. However, in other cases, encapsulating components in this manner can be overly restrictive and lead to unnecessarily complex interfaces should high flexibility be required. It is in these circumstances that the compound component pattern can be particularly useful.
A basic treatment of this pattern for the Toggle component used throughout these examples could look something like:
<Toggle>
<Toggle.Button />
<Toggle.On>Active</Toggle.On>
<Toggle.Off>Inactive</Toggle.Off>
</Toggle>🤓 See Patterns: Basic Compound Component - StackBlitz for an implementation of this basic compound component pattern.
A more flexible treatment of this pattern provides the consumer with more flexibility, particularly with respect to the children that can be supplied to the root Toggle component. This increase in flexibility comes from leveraging React’s context API in place of the Children.map.
🤓 See Patterns: Flexible Compound Component - StackBlitz for an implementation of the flexible compound component pattern.
Sometimes components need to defer control to the consumer in order to allow them to decide what to render and how. A common mechanism for this kind of inversion of control is called a “render prop”. A render prop is simply a prop that accepts a function which the component calls with some agreed API. Typically children is used for this purpose, but any prop can act as a “render prop” for a component.
Applying this pattern to our hypthetical Toggle component provides the consumer with complete control over what is rendered, making Toggle a kind of “headless” component because it does not render any UI itself but instead, manages the relevant state for a "toggle".
<Toggle>
{({ isOn, getTogglerProps }) => (
<Stack direction="row" gap={3}>
<label>{isOn ? 'Active' : 'Inactive'}</label>
<Switch {...getTogglerProps()} />
</Stack>
)}
</Toggle>Another pattern that is often paired with render props is the use of “prop getters”; these are simple functions that encapsulate groups of closely-related props that consumers would otherwise need to manually wire up themselves and typically form part of the render prop's API. In the code snippet above, getTogglerProps is a prop getter.
🤓 See Patterns: Render prop component for an implementation of the render-prop pattern including prop getters.
The more modern cousin of the render prop is the custom hook. In fact, in many cases, it is better to implement shared state and prop getters in a custom hook, like useToggle, than adopt a render prop pattern. This is because hooks generally provide a better mechanism for sharing stateful logic across components than render prop components.
const { isOn, getTogglerProps } = useToggle()
return (
<Stack direction="row" gap={3}>
<label>{isOn ? 'Active' : 'Inactive'}</label>
<Switch {...getTogglerProps()} />
</Stack>
)That said, there are still cases where the render prop pattern is useful. In particular, when a component needs to invert a targeted amount of control to the consumer, it can be useful to provide a render prop for that specific thing.
🤓 See Patterns: Custom Hook for an implementation of the custom hook.