Designing the components tree
When building React Apps, the first step is to think about the components tree. Here are the 5 components I picked for this example:
TodosApp // Rendered once, top-level TodoItem // Rendered for each TODO AddTodoForm // Rendered once FilterButton // Rendered 3 times for filtering TodosLeft // Rendered once
The components I picked for this example are minimal. You can certainly add more components if you want to. For example, you can group the list of TODOs under a TodosList
component or group all action buttons under an ActionButtons
component. You might think of adding a Layout
component to control the layout of the main elements in the App.
Adding more components for readability is often good but keep in mind that adding components that depend on the App state requires having these components receive that state somehow. This often means more complexity and a negative effect on readability.
I often start with the minimal components I can think of and extract new ones as I see the need for them. I think it’s totally okay to start with even a single component and add more only when you have good reasons to do so. However, let me give you some clues to help you come up with an initial design for your components tree.
Planning for components
There are many reasons to make a part of your app into its own component. Many of these reasons fall generally under the readability and reusability categories but some of them are also about maintainability.
A popular reason to introduce a component is to avoid duplication of sub-trees in the main tree or to make parts in the tree reusable in general. Another popular reason is to split big component into smaller ones. However, these are the clues you can use to decide whether or not to extract components from other components.
What clues can you use to plan for the initial shape of your components tree?
Repetition and behavior
One good clue to look for when designing your minimal components is whether an element is going to be repeated (with a loop for example) and especially if the repeated elements are going to have some shared UI behavior. In the simple TODOs example, the TODO items are repeated and they share the UI behavior to flag a TODO as complete or delete it. The filter buttons are also repeated and they share the behavior to modify what TODOs are displayed in the list. These are good clues to decide on a TodoItem
component and a FilterButton
component.
Responsibility isolation
Another clue to look for is whether a certain part of the code is logically grouped with a certain responsibility. In this App, the part where the user needs to type in a new TODO item and submit it to the list is a good example of this. This logically-grouped responsibility is a good candidate to have as a component (which I named AddTodoForm
).
Optimizing renders and computations
Sometimes we introduce a component to optimize the number of times a section of the code is run as a form of "caching" (AKA memoizing) it. You can make a component extend React.PureComponent
instead of React.Component
to make React automatically skip rendering that component if its props and state elements have not changed during a re-render of the parent containing component. If you design your pure component to only depend on the absolute minimum data/state elements that it needs to perform computations, then React’s not re-rendering it would be a form of a one-time cache. That’s the exact thing I did in TodosLeft
to cache the computation of the number of active TODOs left.
When we convert the app to use function components instead of class components we can use the React.memo
function to have a behavior similar to PureComponent
but we’ll need to do a few extra things to exactly match the caching you’re going to see in the class version.
If you use React.PureComponent (or React.memo ) it is extremely important that you do not directly modify (mutate) any arrays or objects which are used by these components. You’ll run into surprising bugs if you mutate them directly.
|
I’ve actually used React.PureComponent
for all components except the root one. Using PureComponent
on the top-level root component is often not necessary especially if the app state is managed on that level. However, the children of a stateful component like TodosApp
(which will re-render frequently) can benefit from being pure. For example, if the state of one TODO item changes from active to complete then the only 2 child components that need to be re-rendered are the TodoItem
component and the TodosLeft
component. We don’t need to re-render the FilterButton
or AddTodoForm
components. If the filter state has changed from "show all" to "show only active" then only the FilterButton
component needs to be re-rendered (to mark the button as active) and nothing else in the app needs to be re-rendered (other than the top-level root component that’s rendering anyhow). We don’t even need to re-render the TodoItem
component in that case (but we do need the root component to unmount some of them). In the next lesson, after explaining the code of the class version, I’ll tell you how you can verify all of this "purity".