This article first appeared on my blog.
0) write it first
React component life cycle: React component life cycle: React component life cycle: React component life cycle: React component life cycle What else is there to say? A waste of affection! Close!)
In general, what we are talking about is the life cycle of a single component. What about between multiple components? Like parent and child components? Sibling components? What about the cycles? What about asynchronous routes? What about the Hooks that came out a while ago? How many people stand up and say I know all about it? (I wouldn’t dare)
I also recently encountered some problems with the life cycle. There are a lot of asynchronous operations involved in the project, so I need to have a clear idea of the order in which the parts are executed.
1) Before you continue
If you don’t know anything about React, this article is probably not for you.
I assume you already know the basics of React, such as the component lifecycle, Hooks concepts, the difference between class components and function components, and have developed complex applications with React.
We are not talking about shouldComponentUpdate(), react.Memo () and other optimizations, just consider the original case.
In this paper, the browser is taken as the target environment. React Native and Electron are the same in basic concept, and the differences in details are not the focus of discussion in this paper.
2) About the life cycle of Hooks
Hooks are not exactly a new component type, they are just a way of reusing code and always come with functional components.
Before Hooks, function components had no concept of state, so there was no lifecycle, just a Render function. Hooks allow function components to have state, which in turn introduces the concept of a lifecycle, specifically useEffect() and useLayoutEffect() when to execute.
Function components are functions by nature, and functions themselves have no lifecycle, and the advent of Hooks does not change that. The object we are talking about here is a “component,” and components can have a life cycle. So when I say Hooks later in this article, I mean “function component that uses Hooks” (which is not strictly, but not important, if you get the idea).
3) So let’s do an experiment
To find out, I wrote a Demo that simulates some common use cases: parent-child components, sibling components, synchronous/asynchronous routing, class components and Hooks, asynchronous operations on component initialization (such as accessing apis), and so on.
If you encounter a usage scenario not covered by Demo, please feel free to make an Issue.
3.1) TL, DR;
I know everyone’s time is very precious, in a hurry friends can directly see the conclusion; For those of you with plenty of time, let’s start with the next section:
- Synchronous routing, parent component in
render
Phase creates child components. - Asynchronous routing, where the parent component starts creating child components after it has mounted itself.
- After the mount is complete, the synchronous component is the same as the asynchronous component when updated.
- Either mount or update to
render
Completion is bounded by the parent component before the child component. - Sibling components are executed roughly in the order they appear in the parent.
useEffect
Execution is delayed after the mount/update is complete.- When an asynchronous request (such as an access API) gets a response is independent of the component’s life cycle, that is, an asynchronous request originating in the parent component is not guaranteed to get a response until the child component finishes mounting.
3.2) Mount process
There are three stages for mounting parent and child components.
In the first stage, the parent component performs its own Render, resolves which sub-components need to be rendered, creates the synchronized sub-components, executes each component one by one to Render, generates the Virtual DOM tree so far, and commits to the DOM.
In the second stage, the DOM node has been generated, the component has been mounted, and the subsequent process begins. ComponentDidMount/useLayoutEffect triggers the parent componentDidMount/useLayoutEffect.
In phase 3, if the component uses useEffect, useEffect is triggered after phase 2. If both parent and child components use useEffect, the child component fires first, followed by the parent component.
If the parent component contains a step-out component, it will be created after the parent component is mounted.
For sibling components, if they are synchronous routes, they are created in the same order as the exit order defined in the parent component.
For “asynchronous sibling components”, I’m not sure if the final loading order is in the order defined in JSX or in the order in which the JS files are downloaded.
In my understanding of “asynchronous”, I prefer to think of the order in which the download is completed, which is more in line with the concept of “loading on demand”.
This is confusing because, as far as I’ve observed so far, the two orders are the same, and I haven’t yet encountered a case where the order is defined later but loaded first.
Most of the time, asynchronous components are divided by page. There are few scenarios where multiple asynchronous components need to be loaded on a single page. Even in these few scenarios, the number of files requested at a time is not large enough to exceed the browser’s concurrency limit; Even if it does, requests are batched in the order of appearance defined in the parent component. Given that individual asynchronous components typically have very small file sizes, very fast loading times, and the same batch of requests arrive almost at the same time, most of the time the download completes in the same order as defined.
However, it does not mean that it does not exist. I will further verify this problem, and those who have the results can also share them.
If a component’s initialization process includes asynchronous operations (typically performed in componentDidMount() and useEffect(fn, [])), when those operations are responded is independent of the component’s life cycle and depends entirely on how long the asynchronous operation itself takes.
3.3) Update process
React is designed to follow a one-way data flow model. Communication between siblings also passes through the parent component (Redux and Context are also implemented by changing the props passed down from the parent component). Therefore, communication between any two components can essentially be attributed to the parent component updating leading to the child component updating.
Updates to parent and child components are also divided into three phases.
The first and third phases are basically the same as the mounting process. In the first phase, a Reconciliation process is added. In the third phase, the Cleanup function of useEffect needs to be executed first.
The second stage, similar to the mount process, is that the child component precedes the parent component, but the update involves more functions than the mount:
getSnapshotBeforeUpdate()
The Cleanup useLayoutEffect ()
useLayoutEffect()
/componentDidUpdate()
React executes these functions in the order shown above, with each function being executed first by its child component and then by its parent. GetSnapshotBeforeUpdate () for each child, then getSnapshotBeforeUpdate() for the parent, then componentDidUpdate() for each child, The parent component’s componentDidUpdate(), and so on.
Here we put the class component and the Hooks lifecycle functions together, because parent components can be any combination of the two component types. Not every function will be useful in actual rendering, only the functions that the component actually owns will be called.
3.4) Unloading process
The uninstallation process involves three functions: Cleanup of componentWillUnmount(), useEffect(), and Cleanup of useLayoutEffect(). The parent component is executed first. The child components execute their respective methods in the order defined in JSX.
Note that the Cleanup function is executed in the order defined in the code, regardless of the function’s nature.
If the uninstallation of the old component is accompanied by the creation of the new component, the new component is first created and executes render, then unloads the old component that is not needed, and finally the new component performs the mounted completed callback.
4) Hooks in particular
React useEffect() and useLayoutEffect() are equivalent to componentDidUpdate()/componentDidMount(), but they differ in some details:
4.1) First come not first go
UseLayoutEffect () always executes before useEffect(), even if useEffect() comes first in your code. So useLayoutEffect() is actually componentDidUpdate()/componentDidMount().
UseEffect () is triggered only after both componentDidUpdate() and componentDidMount() are triggered for the parent component. When both parent and child components use useEffect(), the child component fires before the parent component.
4.2) Disunion Cleanup
As well as having the Cleanup function, useLayoutEffect() and its Cleanup are not necessarily adjacent.
Cleanup of useLayoutEffect() is called between getSnapshotBeforeUpdate() and componentDidUpdate() when the parent component is Hooks and the child component is Class. UseLayoutEffect () is invoked in the same order as componentDidUpdate().
This is the same process with Hooks as child components, only without them it is less obvious.
UseEffect (), on the other hand, is tightly bound to its Cleanup, which never separates from each other.
5) summary
Whether it is class components or Hooks, you are certainly familiar with their lifecycles, but when you mix them together, things get a little more complicated. The process of writing this blog has helped me sort out this mess, and hopefully it has helped you keep seeing it.