React Hooks Deep Dive
Designing Components Tree

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).

It’s all about "encapsulation"

Note how both the repetitions and responsibility clues are in a way related to the general concept of encapsulation: having a feature represented by a single object (component in this case) and having that object reveal what is absolutely necessary for other objects of the application to use it. The clue is to look for any parts in your app that can have self-contained pieces of tasks. Another clue is to identify any component that has too much responsibility in the App and is being used as a dumping ground for many things that do not belong together. Optimize your component design for "future change". Future improvements should ideally require changes in a few places and not have a cascading effect of requiring many changes in many places.

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".