Are there particular guidelines to follow when deciding how to split a React component?
Deciding how to break a component into sub-components isn't easy and is a challenge that isn't specific to React. This is fortunate since it means we can go outside React's ecosystem and get some ideas on how to do it.
In this article, I'll present a guideline to validate ideas on splitting a React component to increase code reuse and reduce maintenance costs. This guideline comes from the paper "Designing Software for Ease of Extension and Contraction" written in 1979 by David Parnas.
To paraphrase Parnas:
Component A is allowed to use component B when all the following conditions
- A is essentially simpler because it uses B.
- B is not substantially more complex because it is not allowed to use A.
- There is a useful subset containing B but not A.
- There is no conceivable useful subset containing A but not B.
Let's clarify a bit each of the criteria:
- Since A will become simpler to implement because it uses B, that relationship makes sense to exist.
- We want to avoid cyclic dependencies for all the known reasons, and we also want to keep our components as simple as possible. Situations where two components benefit from using each other, hint that the decomposition needs some rework.
- It only makes sense for B to exist without A if component B is useful to other components besides A.
- An implementation of A that doesn't have the functionality provided by B doesn't make sense.
For the context of this article, we can consider that the term "use" means to allow a component to reference another one in the code. In truth, it is more nuanced than that, but I won't get into that in this article.
To make this all concrete, let's look at a Video Player component as an example.
The requirements for the Video Player are:
- Optimized for videos with a 16:9 aspect ratio.
- Supports play and pause any time during the video.
- Allows for quick navigation to any part of the video.
- Supports mute and unmute.
- Has full-screen support.
As shown above, VideoPlayer can be decomposed into 4 different components: AspectRatioBox, SliderInput, Button, and Icons. This is not an exhaustive decomposition, but for the purpose of this article, it should be enough.
Let's go over AspectRatioBox and see if it should be its own component according to the guideline.
If VideoPlayer didn't use AspectRatioBox it would have to implement that functionality itself, which would make it more complex than if it used AspectRatioBox.
There's no scenario in which AspectRatioBox would benefit from using VideoPlayer, therefore prohibiting it from using the VideoPlayer won't affect its complexity.
Any time we need to define an element's aspect ratio, the AspectRatioBox will be useful. Hero Images with a background and a grid/list of thumbnails are examples of other situations where the AspectRatioBox would be useful.
Given the requirements for VideoPlayer, I don't see how it could be implemented without the behavior that AspectRatioBox provides.
There will be situations where it isn't obvious if some of the above criteria hold up before starting the implementation. The same can be said about figuring out how to split a component. My suggestion is to first come up with a rough idea on how to split the component, follow it, and keep re-evaluating it as the implementation progresses.
Let's try a slightly different split and see how it holds up:
We've added an ActionsBar component that contains all the actions a user can do. It is supposed to be a simple UI component that receives callbacks for when the user clicks on the buttons. Let's analyze how it holds up:
This one I'm not entirely sure about. ActionsBar would have to receive a lot of callbacks from VideoPlayer would they be separate components, and that could end up resulting in more code cognitive load as we'd be forced to create all of those callbacks and pass them around. If I were to make this separation between VideoPlayer and ActionsBar, I'd keep an eye out during implementation for whether VideoPlayer was simpler because it used ActionsBar or not.
There's no scenario in which ActionsBar would benefit from using VideoPlayer, thus prohibiting it from using the VideoPlayer won't be an issue.
I'd argue that there isn't. The visuals and actions provided by ActionsBar are really specific to VideoPlayer.
Given the requirements for VideoPlayer, it will always need to have the behavior and UI provided by ActionsBar.
As we've seen, ActionsBar is not a good candidate for a component that should exist by itself due to how specific it is to VideoPlayer. Therefore, this decomposition wouldn't likely be one I'd do, and I'd have the behavior and UI given by the ActionsBar be part of VideoPlayer.
In this example, the decomposition was done in terms of UI components, but the same guideline applies to any piece of code that could live in isolation (e.g. hooks, functions, etc).
As a component evolves and gets added functionalities, the initial decomposition will get outdated and we'll have to think of a new one. Hopefully, with this approach, we should still be able to reuse many of the components we initially had.
This guideline aims at splitting a component into multiple ones that can be reused across the same or different applications. Inside the component itself, we may still opt to split it further for other reasons such as improving performance.
Next time you're developing a new component try to use this guideline to decompose it into reusable pieces.
- A is essentially simpler because it uses B
- B is not substantially more complex because it is not allowed to use A
- There is a useful subset containing B but not A
- There is no conceivable useful subset containing A but not B
I'd suggest coming up with a decomposition before starting to code, and as you go on and learn more about the component you're writing, adjust the decomposition accordingly.
Also, keep in mind that you're the only person that knows the context you're in. So don't follow the guideline blindly and check that it makes sense in the context you're in.