Techniques for decomposing React components
React components have a lot of power and flexibility. With so many tools at your disposal, it is incredibly easy for components to grow over time, become bloated and do too much.
As with any other kind of programming, adhering to the single responsibility principle not only makes your components easier to maintain, but also allows for greater reuse. However, identifying how to separate the responsibilities of a large React component is not always easy. Here are three techniques to get you started, from the simplest to most advanced.
Split the render() method
This is the most obvious technique to apply: when a component renders too many elements, splitting those elements into logical sub-components is an easy way to simplify.
A common and quick way to split the
render() method is to create additional “sub-render” methods on the same class:
While this can have its place, it is not a true decomposition of the component itself. Any state, props, and class methods are still shared, making it difficult to identify which of those are used by each sub-render.
To really reduce complexity, entirely new components should be created instead. For simpler sub-components, functional components can be used to keep boilerplate to a minimum:
There is another subtle but important difference achieved by splitting in this way. By replacing direct function calls with indirect component declarations, we produce smaller units for React to work with. This is because the return value of
render() is a tree of element descriptors that only go as far as
PanelBody, rather than all the elements beneath them too.
There are practical implications for testing: a shallow render can be used to easily isolate those units for independent testing. As a bonus, when React’s new Fiber architecture arrives, the smaller units will allow it to perform incremental rendering more effectively.
Templatize components by passing React elements as props
If a component is becoming too complex due to multiple variations or configurations, consider turning the component into a simple “template” component with one or more open “slots”. This allows a dedicated parent component to focus on configuration only.
For example, a
Comment component may have different actions and metadata displayed depending on whether you are the author, whether it was successfully saved, or what permissions you have. Rather than mix the structure of the
Comment component — where and how its content is rendered — with logic to handle all the possible variations, these two concerns can be considered independently. Utilise the ability of React to pass elements, not just data, as props, to create a flexible template component.
Another component can then have the sole responsibility of figuring out what to fill the
actions slots with.
Keep in mind that in JSX, anything that’s between the opening and closing tags of a component is passed as the special
children prop. This can be particularly expressive when used correctly. To be idiomatic, it should be reserved for the main content area of a component. In the comments example, this would likely be the text of the comment itself.
Extract common aspects into higher-order components
Components can often become polluted by cross-cutting concerns that aren’t directly related to its main purpose.
Suppose you wanted to send analytics data whenever a link in a
Document component is clicked. To further complicate things, the data needs to include information about the document such as its ID. The obvious solution might be to add code to
componentWillUnmount lifecycle methods, like so:
There are a few problems with this:
- The component now has an extra concern that obscures its main purpose: displaying a document.
- If the component has additional logic in those lifecycle methods, the analytics code also becomes obscured.
- The analytics code is not reusable.
- Refactoring the component is made harder, as you’d have to work around the analytics code.
Decomposition of aspects such as this one can be done using higher-order components (HOCs). In short, these are functions that can be applied to any React component, wrapping that component with a desired behaviour.
It’s important to note that this function doesn’t mutate the component to add its behaviour, but returns a new wrapping component. It is this new wrapping component that is used in place of the original
Notice that the specific detail of what data to send (
documentId) can be extracted as configuration for the HOC. This keeps the domain knowledge of documents with the
Document component, and the generic ability to listen to clicks in the
Higher-order components show off the powerful compositional nature of React. The simple example here demonstrates how seemingly tightly-integrated code can be extracted into modules with single responsibilities.
HOCs are commonly employed in React libraries such as react-redux, styled-components, and react-intl. After all, these libraries are all about solving generic aspects of any React application. Another library, recompose, goes a step further by using HOCs for everything from component state to lifecycle methods.
React components are highly composable by design. Use this to your advantage by readily decomposing and composing them.
Don’t shy away from creating small, focused components. It may feel awkward at first, but the result will be code that is more robust and reusable.