- Indepth.dev /posts/1009/…
- In-depth Explanation of state and props update In React
- Originally written by Max Koretskyi
The basics needed to understand the details of the update process were covered in my previous article Inside Fiber: An In-depth look at the React New Coordination algorithm, which I’ll describe in this article.
I have outlined the main data structures and concepts that will be used in this article, particularly Fiber nodes, current and work-in-Progress trees, side-effects, and Effects linked lists. I also provided a high-level overview of the main algorithms and the differences between the Render phase and the COMMIT phase. If you haven’t read it yet, I recommend you start there.
I also showed you an example program with a button that simply adds numbers.
You can view the code online here. It is simply implemented as a class component in the Render function that returns button and SPAN elements. Update the component’s state in the click event handler when you click the button. The result is that the text of the SPAN element is updated.
class ClickCounter extends React.Component {
constructor(props) {
super(props);
this.state = {count: 0};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState((state) = > {
return {count: state.count + 1};
});
}
componentDidUpdate() {}
render() {
return [
<button key="1" onClick={this.handleClick}>Update counter</button>.<span key="2">{this.state.count}</span>]}}Copy the code
I added the componentDidUpdate lifecycle method to this component. This is to demonstrate how React adds effects and calls this method in the COMMIT phase. In this article, I want to show you how React handles status updates and creates Effects Lists. We can see what happens in the high-level functions of the Render phase and the Commit phase.
Especially in the React completeWork function:
- update
ClickCounter
thestate
In thecount
attribute - call
render
Method to get a list of child elements and compare - update
span
Elements of theprops
And, in the React commitRoot function:
- update
span
Element’s text content property - call
componentDidUpdate
Lifecycle approach
But before we do that, let’s take a quick look at how work is scheduled when we call setState in our click-handler function.
Note that you don’t need to know this to use React. This article is about how React works internally.
Schedule updated
When we click the button, the click event is triggered and React performs a callback passed to the button props. In our program, it simply increments the counter and updates the state.
class ClickCounter extends React.Component {...handleClick() {
this.setState((state) = > {
return {count: state.count + 1}; }); }}Copy the code
Each component has a corresponding updater, which acts as a bridge between the component and the React core. This allows setState to be implemented differently in ReactDOM, React Native, server render, and test applications. (Updater. enqueueSetState is called internally by setState to update the page on any platform.)
In this article, we focus on the updater object implemented in the ReactDOM, which uses the Fiber coordinator. For the ClickCounter component, it is a classComponentUpdater. It is responsible for getting Fiber instances, enlisting for updates, and scheduling work.
When updates are queued, they are basically just added to the update queue of the Fiber node for processing. In our example, the Fiber node corresponding to the ClickCounter component will have the following structure:
{
stateNode: new ClickCounter,
type: ClickCounter,
updateQueue: {
baseState: {count: 0}
firstUpdate: {
next: {
payload: (state) = > { return {count: state.count + 1}}}},... },... }Copy the code
As you can see, updateQueue. FirstUpdate. Next, the function in the content as far as I’m in our ClickCounter component is passed to the callback setState. It represents the first update that needs to be processed in the Render phase.
Handle updates to ClickCounter Fiber nodes
The role of the global variable nextUnitOfWork was explained in the work loop section of my previous article. In particular, this variable holds a reference to a Fiber node in the workInProgress tree that has work to do. When React traverses the Fiber tree, it uses this variable to know if there are other Fiber nodes with unfinished work.
We assume that the setState method has already been called. React adds the setState callback to the updateQueue of the ClickCounterfiber node and schedules the work. React enters the render phase. It uses the renderRoot function to traverse from the topmost HostRootFiber node. However, it skips Fiber nodes that have already been processed until it encounters nodes with unfinished work. At this point, only one node has work to do. It is the ClickCounterFiber node.
All work is performed based on a clone copy of the Alternate field stored in the Fiber node. If the alternate node has not been created, React calls createWorkInProgress to create a copy before processing the update. We assume that the nextUnitOfWork variable holds a reference to the ClickCounterFiber node instead.
beginWork
First, our Fiber goes into the beginWork function.
Since this function is executed on every node in the tree, it is a good place to place breakpoints if you want to debug the Render phase. I do this a lot, and also check the Fiber node type to determine which node I need.
The beginWork function is basically a large switch statement that identifies the type of work the Fiber node needs to do with a tag, and then executes the corresponding function to perform the work. In this example, CountClicks is a class component, so it goes through this branch:
function beginWork(current$$1, workInProgress, ...) {...switch (workInProgress.tag) {
...
caseFunctionalComponent: {... }case ClassComponent:
{
...
returnupdateClassComponent(current$$1, workInProgress, ...) ; }caseHostComponent: {... }case. }Copy the code
We go to the updateClassComponent function. Depending on whether it’s rendering for the first time, restoring work, or updating React, React creates the instance and mounts the component or just updates it:
function updateClassComponent(current, workInProgress, Component, ...) {...const instance = workInProgress.stateNode;
let shouldUpdate;
if (instance === null) {...// In the initial pass we might need to construct the instance.constructClassInstance(workInProgress, Component, ...) ; mountClassInstance(workInProgress, Component, ...) ; shouldUpdate =true;
} else if (current === null) {
// In a resume, we'll already have an instance we can reuse.shouldUpdate = resumeMountClassInstance(workInProgress, Component, ...) ; }else{ shouldUpdate = updateClassInstance(current, workInProgress, ...) ; }returnfinishClassComponent(current, workInProgress, Component, shouldUpdate, ...) ; }Copy the code
Handle ClickCounter Fiber updates
We already have the ClickCounter component instance, so let’s go to updateClassInstance. This is where React does most of the work for the class components. Here are the most important operations performed in this function in order:
- call
UNSAFE_componentWillReceiveProps()
Hook (obsolete) - To deal with
updateQueue
Update and generate new states in - Use the new state call
getDerivedStateFromProps
And get the results - call
shouldComponentUpdate
Determine if the component needs to be updated; If the return result isfalse
, skips the entire rendering process, including calls on this component and its childrenrender
; Otherwise continue to update - call
UNSAFE_componentWillUpdate
(Abandoned) - Add an effect to trigger
componentDidUpdate
Lifecycle hook
Although the effect calling componentDidUpdate is added during the Render phase, this method will be executed during the subsequent COMMIT phase.
- Updates the component instance
state
andprops
The component instance’s state and props should be updated before the Render method is called, because the output of the Render method usually depends on state and props. If we don’t, it will return the same output every time.
Here is a simplified version of this function:
function updateClassInstance(current, workInProgress, ctor, newProps, ...) {
const instance = workInProgress.stateNode;
const oldProps = workInProgress.memoizedProps;
instance.props = oldProps;
if(oldProps ! == newProps) { callComponentWillReceiveProps(workInProgress, instance, newProps, ...) ; }let updateQueue = workInProgress.updateQueue;
if(updateQueue ! = =null) { processUpdateQueue(workInProgress, updateQueue, ...) ; newState = workInProgress.memoizedState; } applyDerivedStateFromProps(workInProgress, ...) ; newState = workInProgress.memoizedState;constshouldUpdate = checkShouldComponentUpdate(workInProgress, ctor, ...) ;if (shouldUpdate) {
instance.componentWillUpdate(newProps, newState, nextContext);
workInProgress.effectTag |= Update;
workInProgress.effectTag |= Snapshot;
}
instance.props = newProps;
instance.state = newState;
return shouldUpdate;
}
Copy the code
I removed some of the helper code from the above snippet. For instances, React uses the Typeof operator to check that the component implements the lifecycle methods before calling them or adding effects to trigger them. For example, here’s how to check componentDidUpdate before adding an effect to React:
if (typeof instance.componentDidUpdate === 'function') {
workInProgress.effectTag |= Update;
}
Copy the code
Ok, we now know what was done for the ClickCounterFiber node in the Render phase. Now let’s see how these operations change the value of the Fiber node. When React starts working, the ClickCounter component’s Fiber node looks something like this:
{
effectTag: 0.elementType: class ClickCounter.firstEffect: null.memoizedState: {count: 0},
type: class ClickCounter.stateNode: {
state: {count: 0}},updateQueue: {
baseState: {count: 0},
firstUpdate: {
next: {
payload: (state, props) = >{... }}},... }}Copy the code
After the work is done, we have a Fiber node that looks like this:
{
effectTag: 4.elementType: class ClickCounter.firstEffect: null.memoizedState: {count: 1},
type: class ClickCounter.stateNode: {
state: {count: 1}},updateQueue: {
baseState: {count: 1},
firstUpdate: null. }}Copy the code
Take the time to observe the difference in attribute values
After the update is applied, count of the baseState property in memoizedState and updateQueue changes to 1. React also updated the state of the ClickCounter component instance.
At this point, there are no more updates in the queue, so firstUpdate is null. More importantly, we changed the effectTag property. It’s no longer 0, it’s 4. A binary value of 100 means that the third bit is set to represent the Update side effect flag:
export const Update = 0b00000000100;
Copy the code
It can be concluded that when performing the ClickCounterFiber node’s work, React uses the pre-change lifecycle method to update state and define associated side effects.
Coordinate ClickCounter Fiber sub-components
After that, React enters the finishClassComponent. This is where the render method of the component instance is called and the diff algorithm is used on the child components. A high-level overview of this is provided in the documentation. Here’s the relevant part:
When comparing two React DOM elements of the same type, React looks at the attributes of both, leaving the DOM node intact and updating only the changed attributes.
However, if we dig a little deeper, we’ll see that it actually compares the Fiber node to the React element. But I’m not going to go into that right now because it’s pretty complicated. In a separate article, I will focus specifically on the sub-coordination process.
If you want to learn the details for yourself, check out the reconcileChildrenArray function, because in our program the Render method returns an array of React elements.
There are two important things to understand at this point. First, when React performs subcoordination, it creates or updates Fiber nodes for the child React elements returned from the Render function. Reference to the first child of the current Fiber node in the finishClassComponent function. It is assigned to nextUnitOfWork and processed later in the work loop. Second, React updates the props of the child node as part of the work performed by the parent node. To do this, it uses the Render function to return the React element’s data.
For example, this is how the Fiber node corresponding to the SPAN element looks before React coordinates ClickCounterfiber child nodes
{
stateNode: new HTMLSpanElement,
type: "span".key: "2".memoizedProps: {children: 0},
pendingProps: {children: 0},... }Copy the code
As you can see, the children property of memoizedProps and pendingProps is 0. This is the structure of the React element corresponding to the SPAN element returned by the Render function.
{
$$typeof: Symbol(react.element)
key: "2"
props: {children: 1}
ref: null
type: "span"
}
Copy the code
As you can see, the props of the Finer node and the React element returned are different. CreateWorkInProgress internally uses this to create a replacement Fiber node. React copies updated properties from the React element to the Fiber node.
Therefore, after React completes the ClickCounter component sub-coordination, the Span Fiber node’s pendingProps are updated. They will match the values in the spanReact element.
{
stateNode: new HTMLSpanElement,
type: "span".key: "2".memoizedProps: {children: 0},
pendingProps: {children: 1},... }Copy the code
Later, React will perform work for the spanFiber nodes, which will copy them to memoizedProps and add effects to update the DOM.
Ok, that’s all the work the Render phase React does for the ClickCounterfiber node. Because Button is the first child of the ClickCounter component, it is assigned to the nextUnitOfWork variable. With nothing to do on the Button, React will move to its sibling spanFiber node. According to the algorithm described here, this happens inside the completeUnitOfWork function.
Handle Span Fiber updates
The nextUnitOfWork variable now points to spanfiber’s alternate, and React starts working on it. Similar to the steps performed by ClickCounter, you start with the beginWork function.
Since the span node is of HostComponent type, React will enter this branch in the switch statement:
function beginWork(current$$1, workInProgress, ...) {...switch (workInProgress.tag) {
caseFunctionalComponent: {... }caseClassComponent: {... }case HostComponent:
returnupdateHostComponent(current, workInProgress, ...) ;case. }Copy the code
End with the updateHostComponent function. You can see a series of functions similar to the updateClassComponent function called by the class component. For the function component it is updateFunctionComponent. You can find these functions in the reactFiberBeginwork.js file.
Coordinate Span Fiber child nodes
In our example, nothing important happens to the SPAN node in the updateHostComponent.
Complete the Span Fiber node work
Once the beginWork completes, the node enters the completeWork function. But before that happens, React needs to update the memoizedProps property of the Span Fiber node. You will recall updating the pendingProps for the spanFiber node when coordinating ClickCounter component child nodes.
{
stateNode: new HTMLSpanElement,
type: "span".key: "2".memoizedProps: {children: 0},
pendingProps: {children: 1},... }Copy the code
So once the SpanFiber beginWork is complete, React will update the pendingProps to memoizedProps.
function performUnitOfWork(workInProgress) {... next = beginWork(current$$1, workInProgress, nextRenderExpirationTime); workInProgress.memoizedProps = workInProgress.pendingProps; . }Copy the code
The completeWork is then called similar to the beginWork we saw, which is basically a big switch statement.
function completeWork(current, workInProgress, ...) {...switch (workInProgress.tag) {
caseFunctionComponent: {... }caseClassComponent: {... }caseHostComponent: { ... updateHostComponent(current, workInProgress, ...) ; }case. }}Copy the code
Since the spanFiber node is a HostComponent, it performs the updateHostComponent function. In this function React basically does these things:
- Prepare DOM updates
- Let’s add them to PI
span
The fiberupdateQueue
- Add an effect to update the DOM
Before these operations are performed, the spanFiber node looks like this:
{
stateNode: new HTMLSpanElement,
type: "span".effectTag: 0
updateQueue: null. }Copy the code
When works is complete it will look like this:
{
stateNode: new HTMLSpanElement,
type: "span".effectTag: 4.updateQueue: ["children"."1"],... }Copy the code
Notice the difference between the effectTag and updateQueue fields. It’s no longer 0, it’s 4. The binary representation of 100 means that bit 3 is set, which is the marker for Update side effects. This is the only task React will do for this node during the subsequent commit phase. UpdateQueue holds the payloads used for updates.
Once React processes the ClickCounter level and its children, the Render phase ends. It can now assign the completed replacement tree to the finishedWork property of FiberRoot. This is the new tree that needs to be refreshed to the screen. It can be processed immediately after the Render phase, or when React is given time by the browser.
Effects list
In our example, React will add the firstEffect property that links to HostFiber to the spanFiber node because the SPAN node ClickCounter component has side effects.
React builds the Effects List inside the compliteUnitOfWork function. Here’s what a Fiber tree looks like with the side effects of updating the span node text and calling hooks on ClickCounter:
Here is a linear list of nodes with side effects:
The Commit phase
This stage starts with the completeRoot function. Before it does anything else, it sets the FiberRoot finishedWork property to null:
root.finishedWork = null;
Copy the code
Unlike the previous Render phase, the Commit phase is always synchronized so that it can safely update HostRoot to indicate that the commit work has started.
The COMMIT phase is where React updates the DOM and calls the mutated lifecycle method componentDidUpdate. To do this, it iterates through the Effects List built in the Render phase and applies them.
There are the following effects defined in the Render phase for SPAN and ClickCounter:
{ type: ClickCounter, effectTag: 5 }
{ type: 'span'.effectTag: 4 }
Copy the code
ClickCounter’s Effect tag, which has a value of 5 or binary 101, defines the Update work for class components that are essentially converted to the componentDidUpdate lifecycle method. The lowest level is also set, indicating that all work on the Fiber node is done in the Render phase.
The span effect tag, which has a value of 4 or binary 100, defines the update work for native component DOM updates. For the span element in this example, React needs to update its textContent.
The application effects
Let’s see how React applies these effects. The commitRoot function applies these effects and consists of three subfunctions:
function commitRoot(root, finishedWork) {
commitBeforeMutationLifecycles()
commitAllHostEffects();
root.current = finishedWork;
commitAllLifeCycles();
}
Copy the code
Each child function implements a loop that iterates through the effects list and checks the types of those effects. Effect is applied when it is found to be related to the purpose of the function. In our case, it calls the ClickCounter component’s componentDidUpdate lifecycle method to update the text of the SPAN element.
The first function commitBeforeMutationLifeCycles find the Snapshot effect and then call getSnapshotBeforeUpdate method. However, we didn’t implement this method in the ClickCounter component, and React didn’t add this effect in the Render phase. So in our example, this function doesn’t do anything.
DOM updates
React then executes the commitAllHostEffects function. Here’s where React changes the t text of the SPAN element from 0 to 1. ClickCounter Fiber has nothing to do because the nodes of the class component don’t have any DOM updates.
The purpose of this function is to select the right type of effect and apply the corresponding action. In our example we need the text with the new SPAN element, so we use the Update branch:
function updateHostEffects() {
switch (primaryEffectTag) {
casePlacement: {... }casePlacementAndUpdate: {... }case Update:
{
var current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
caseDeletion: {... }}}Copy the code
With commitWork, the updateDOMProperties function is eventually entered. It updates the textContent of the Span element with the updateQueue payload added to the Fiber node during the Render phase.
function updateDOMProperties(domElement, updatePayload, ...) {
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
if(propKey === STYLE) { ... }else if(propKey === DANGEROUSLY_SET_INNER_HTML) {... }else if (propKey === CHILDREN) {
setTextContent(domElement, propValue);
} else{... }}}Copy the code
After applying the DOM update, React assigns finishedWork to HostRoot. It sets the alternative tree to the current tree:
root.current = finishedWork;
Copy the code
Call post-mutation life cycle hooks
The remaining functions are commitAllLifecycles. This is where React calls the mutated lifecycle method. In the Render phase, React adds the Update effect to the ClickCounter component. This is one of the effects commitAllLifecycles looks for and calls the componentDidUpdate method:
function commitAllLifeCycles(finishedRoot, ...) {
while(nextEffect ! = =null) {
const effectTag = nextEffect.effectTag;
if (effectTag & (Update | Callback)) {
constcurrent = nextEffect.alternate; commitLifeCycles(finishedRoot, current, nextEffect, ...) ; }if(effectTag & Ref) { commitAttachRef(nextEffect); } nextEffect = nextEffect.nextEffect; }}Copy the code
This function also updates refs, but since we don’t use this feature, it doesn’t do much. This method is called in the commitLifeCycles function:
function commitLifeCycles(finishedRoot, current, ...) {...switch (finishedWork.tag) {
caseFunctionComponent: {... }case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.effectTag & Update) {
if (current === null) {
instance.componentDidMount();
} else{... instance.componentDidUpdate(prevProps, prevState, ...) ; }}}caseHostComponent: {... }case. }Copy the code
You can also see that this is the function that calls the component componentDidMount method when rendering the first time.