In this post we’ll run a few experiments to see how React’s renderer works and gain a understanding of some of its guaranteed (spoiler: I’ll use this word a lot 😅) behaviors.
First Experiment and Conventions
Every experiment consists of two elements: a graph and an events table:
Let’s start with our first experiment so you can get familiar with them.
We’ll start with simple component hierarchy consisting of a root component and two children components “A” and “B”:
When you run an application like this, the following events will happen in succession:
Note: This is a simple colored output from Firefox DevTools used to describe a sequence of events. A green “Rendered” means that a component is rendered (i.e: it’s function body is called) and a blue “useEffect” means that an effect inside a component was run. Every log is accompanied with a colored label to make it easier to follow along.
As you can see, first, every component is rendered from top to bottom starting from the root component followed by each children. Then, effects are run on the opposite side, from bottom to top.
Since our components is relatively simple, it doesn’t give us the whole picture of renderer’s behavior. Let’s add more complexity to our tree.
To gain more insight about the rendering process, let’s add a few more nodes to our tree:
The events for this tree are as follows:
We keep the same behavior than before: tree is rendered from top to bottom and each child is traversed too. As you can see, there’s an interesting thing: nodes are traversed in sequence (from “left” to “right”). However, this is behavior from React’s current implementation and not guaranteed behavior, so don’t rely on this by any means! (Actually, why you would need to rely on this?)
On the effects side, we can see that they’re run bottom to top. It’s guaranteed that children effects will run before the ones from the parent but sibling effects’ order aren’t guaranteed to run in any order (Again, why would you need that?).
Until now, everything we analyzed was the first render behavior. We haven’t tested what happens on re-renders and how effects’ cleanups behave.
Let’s start with a leaf node (a node without childrens). When you click the node “3”, it is rendered again and its previous associated effects are cleanup before triggering them again with the new props and state:
React docs says that “The clean-up function runs before the component is removed from the UI to prevent memory leaks” and that on re-renders, any “previous effect is cleaned up before executing the next effect”.
When you click on a node that has childrens, it calls the render method for each one of them and then proceeds to cleanup previous effects and run the new ones on a per-component basis:
An important (and guaranteed) behavior to notice is that React runs all childrens’ effects and cleanups before the ones from the parent.
As we saw before, there are some consistent behavior in the React rendering process. However, the current implementation doesn’t guarantee anything about future behaviors. There are only a few documented guaranteed behaviors that React guarantees and that you can take advantage of:
- All previous effects are cleaned up before apply the new ones when a component updates.
- DOM will be updated before run effects.
- Children cleanups and effects run before the ones from its parent.
- Effects are run after layout and paint
React doesn’t guarantee the following:
- Renders are not guaranteed to happen in sequence (for example, left to right).
- Effects within a component are not guaranteed to happen in sequence.
I want to be clear that not guaranteed doesn’t mean that it was necessarily discussed and decided in this way. In can change in the future, but for now, your components should only make assumptions about guaranteed behavior. Any current behavior can change in future React versions without previous notice.
A good thing about not guaranteed certain behaviors is that you have more flexibility. React Fiber is a great example about this. In a very few words, React Fiber is an internal implementation (thus, you don’t need to learn it to use React) that allows among other things to pause work and continue it later, assign priority to different types of work, and abort work if it’s no longer needed. This is very useful if you have different types of work that needs to be done. For example:
- Give priority to animations.
- Give lower priority to off-screen elements.
- Process user actions as soon as possible.
In the classic React’s renderer, it’s not possible to do this kind of things. However, by not guaranteeing that renders happen in sequence, this is completely possible and way easier to do it in a backwards compatible way than without it.
We make real experiments with React and learned how it internally handles rendering. We started with a simple version and increase complexity gradually to understand how rendering works.
We were able to discover what React does and doesn’t guarantee, so you can get back to this in case you need it in the future :-) However, I don’t think most of the time you need to be aware of this and so, it isn’t needed to code in React, but I think it’s fun to know this little details!
If you want to go (really) deep on React’s rendering behavior, check Mark Erikson’s article A (Mostly) Complete Guide to React Rendering Behavior.