iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
⚙️

Understanding How React 18 Works Through Visuals

に公開

What is this?

This article summarizes how React 18 works. I have minimized the use of code and focused on readability by using diagrams. Also, for those who plan to read React's internal source code in the future, I have included accordion-style links to GitHub at the end of each section.
*This article is for the 17th day of the note Inc. Advent Calendar 2022.

Target Audience

  • Those who don't intend to read React's internal code but want to understand what is happening under the hood.
  • Those who are planning to read React's internal code.
  • Those who have some free time.

About Fiber

First, I would like to explain "Fiber," which appears frequently when you are looking through React documentation.

What is Fiber in the first place?

With some exceptions, one Fiber is an object that manages one component (such as <MyComponent> or <div>). React generates a "Fiber tree," not a "DOM tree," to use for the rendering process. It is also a unit of work. When React builds the actual HTML from the Fiber tree, it can temporarily pause work at appropriate timings if necessary. That pause point is the Fiber unit.

Why use Fiber?

By wrapping actual DOM nodes in objects called Fibers, we can manage user-defined components like <MyComponent> and React's unique concepts, such as priority information, as a set.

What is the structure of the Fiber tree?

Let's see what kind of Fiber is created with the following code. When you run this, HTML like <div>Hello<span>world</span></div> will be generated inside div#root, and the text "Helloworld" will be displayed. For the sake of explanation, I am using createElement instead of JSX.

<html>
  <body>
    <script src="./node_modules/react/umd/react.development.js"></script>
    <script src="./node_modules/react-dom/umd/react-dom.development.js"></script>

    <div id="root"><!-- React output goes here --></div>
	  
    <script>
      const App = () => {
        const child = React.createElement('span', null, 'world');
        return React.createElement('div', null, 'Hello', child);
      }
      const root = ReactDOM.createRoot(
        document.getElementById('root')
      );
      root.render(React.createElement(App, {}, null));
    </script>
  </body>
</html>

The following Fiber tree is generated inside React.

You can see that it is almost the same as the DOM tree.
The major differences between the DOM tree and the Fiber tree are the following three:

  • It includes user-defined components like <App> as part of the tree.
  • A "FiberRootNode" exists to hold the final display destination (in this case, div#root).
  • A node called "HostRoot" representing the root exists.

There are two types of object structures in the Fiber tree above.

FiberNode

Nodes represented in green. You can access your child, sibling, and parent using the child, sibling, and return properties.

FiberRootNode

Nodes represented in red. It has a different object structure from "FiberNode." This node manages the final display destination (in this case, div#root) and you can access div#root via the containerInfo property.

Tracing the Flow of Initial Rendering

React uses the Fiber tree for rendering.
Now, let's trace the flow until the content is displayed on the screen using the code below, to understand how React actually uses the Fiber tree for rendering. I have marked points of note with a "★".

<html>
  <body>
    <script src="./node_modules/react/umd/react.development.js"></script>
    <script src="./node_modules/react-dom/umd/react-dom.development.js"></script>

    <div id="root"><!-- React output goes here --></div>
	  
    <script>
      const App = () => {
        const child = React.createElement('span', null, 'world');
        return React.createElement('div', null, 'Hello', child);
      }
      // ★1 Create "FiberRootNode" and "HostRoot"
      const root = ReactDOM.createRoot(
        document.getElementById('root')
      );  
	    
      // ★2 Create React element of App() with createElement
      reactElement = React.createElement(App, {}, null)
	    
      // ★3 Create Fiber tree from React element, then perform rendering.
      root.render(reactElement);  
    </script>
  </body>
</html>

★1 Create "FiberRootNode"

When ReactDOM.createRoot is called, the createFiberRoot function is called internally, and "FiberRootNode" and "HostRoot" are created.

Peek at the actual code

It looks like this after execution.

★2 Create React element with createElement

Next, we create an object called a React element using React.createElement. React elements are necessary to execute root.render.

It is confusing because there are many similar concepts, but Fiber and React elements are different objects. If you use JSX, you don't have many opportunities to write React.createElement, but since JSX is converted to React.createElement during transpilation, the flow of creating React elements is the same. [1]

★3 Create Fiber tree from React element, then perform rendering.

When you pass a React element to root.render and execute it, the rendering process is finally performed.
The steps to rendering are the following three:

  1. While converting React elements to Fibers, build the tree until reaching the terminal nodes (leaves).
  2. Once reaching the terminals, convert Fibers to DOM nodes and store them in stateNode while returning up the tree.
  3. After returning to the root, appendChild (add) the completed DOM nodes to div#root and finish.

I have created a GIF animation of the flow of 1. and 2. to make it easier to understand.
stateNode is one of the properties a Fiber holds, and it stores all the DOMs of itself and its child elements merged together. You can see how the DOM is gradually built as it returns up the tree. However, function components (like App in this case) do not have a stateNode and contain null. (Please click to enlarge if the image is too small to see.)

After 1. and 2. are completed, the stateNode closest to the root, excluding null, is added to div#root.
With this, the text "Helloworld" is displayed on the browser, and rendering is complete.

Peek at the actual code

In the actual code, the tree is built recursively with workLoopSync below.
The variable workInProgress contains the Fiber currently being processed, and its value changes every time the performUnitOfWork function is executed.

https://github.com/facebook/react/blob/edbfc6399ffa31267a2e884a63194b5ea17514ff/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L2131-L2138

Let's take a look at performUnitOfWork. beginWork is the function that actually builds the Fiber tree. After that, if there is a next (the child Fiber to be processed next), it repeats the loop, and if not, it executes the completeUnitOfWork function. This completeUnitOfWork function is the step that constructs the stateNode while returning up the tree in 3.

https://github.com/facebook/react/blob/edbfc6399ffa31267a2e884a63194b5ea17514ff/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L2314-L2340

About the Rendering Phase

There are two phases to rendering

We have looked at the flow of the initial rendering so far. In fact, the rendering process we just looked at can be classified into two phases: the "Render phase" and the "Commit phase."

The "Render phase" refers to the work of converting React elements into Fibers, constructing the stateNode, and preparing everything right up to the point of updating the DOM. Then, the "Commit phase" is the work of actually updating the DOM.

It is important to note that React has the ability to pause and resume work during the "Render phase." Because JavaScript in browsers is single-threaded, if the processing of the "Render phase" is too heavy (for example, if a page has a massive number of DOM elements), the browser will freeze until the work is finished. However, by temporarily pausing the work, React can relinquish the precious single thread to JavaScript, allowing it to periodically ensure time to process user operation events, such as click events.

On the other hand, the "Commit phase" is not interrupted once it begins. While the "Commit phase" might cause the browser to freeze if it performs extremely heavy processing, the work it actually does is relatively short-lived, which is just modifying the DOM using the stateNode that was prepared in advance. The design is such that as much work as possible is done in the "Render phase," and the "Commit phase" is kept as short as possible.

Even so, is most of the Render phase not interrupted?

So, in what cases can the "Render phase" actually be interrupted?
I have investigated various sources, and according to the following issue, it seems that in v18, the cases where it is interrupted are limited to when startTransition or Suspense is used.

https://github.com/facebook/react/issues/24392

It does not automatically pause for you under the hood; you need to wrap heavy processing with startTransition or similar. For example, to make the initial rendering interruptible, you would write it wrapped as follows (though even in this case, it only becomes interruptible for the initial rendering, and subsequent processes generally continue to run without interruption).

React.startTransition(() => {
  root.render(React.createElement(App, {}, null));
})

What is happening inside React? Internally, the pattern that permits interruption of the "Render mode" is defined as the workLoopConcurrent function, and the pattern that does not permit it is defined as the workLoopSync function.
The difference between the two functions is just whether or not they call shouldYield.

Peek at the actual code

shouldYield determines whether frameYieldMs milliseconds (likely a fixed value of 5ms) have elapsed since the start of the "Render phase," and if so, it interrupts the "Render phase." On the other hand, the workLoopSync function continues to work as long as there is remaining work (workInProgress, i.e., Fiber) regardless of the elapsed time.

Then, under what conditions do workLoopConcurrent and workLoopSync switch?

This is where the new concept of "Lanes" comes in.

Looking at the caller, within the includesBlockingLane function, if the current work is included in the following lanes, it seems to be treated as uninterruptible.

Uninterruptible Lanes

  • InputContinuousHydrationLane
  • InputContinuousLane
  • DefaultHydrationLane
  • DefaultLane
Peek at the actual code

shouldTimeSlice is the boolean for whether it is interruptible. Strictly speaking, includesExpiredLane is also included in the judgment, but since I assume this is a relief measure for tasks that have been put off for a long time to perform them without interruption, I have omitted it.
https://github.com/facebook/react/blob/9e3b772b8cabbd8cadc7522ebe3dde3279e79d9e/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L866-L878

includeBlockingLane determines that the work cannot be interrupted if it is one of InputContinuousHydrationLane, InputContinuousLane, DefaultHydrationLane, or DefaultLane.
https://github.com/facebook/react/blob/9e3b772b8cabbd8cadc7522ebe3dde3279e79d9e/packages/react-reconciler/src/ReactFiberLane.old.js#L466-L480

What are lanes?

About Lanes

Lanes represent priority

Lanes are flags made from 32-bit bitmasks, used only within React.
Each bit indicates what kind of task it represents, and also represents its priority. Lanes closer to 0 have higher priority, while those further away have lower priority.

As a special lane, there is a lane called "NoLane" where all bits are 0.
This is the default value when no lane is set, similar to undefined in JavaScript, and it has the highest priority. (Though it is rare to use "NoLane" as a priority.)

These constants are defined in ReactFiberLane.js.

Peek at the actual code

I will briefly explain the features of lanes. By using operators, you can merge lanes together or check if a target lane is included. It is also possible to create a bitmask that has multiple lanes.

const SyncLane = 0b0000000000000000000000000000001;
const InputContinuousLane = 0b0000000000000000000000000000100;

// Merge lanes with `|`
// Becomes a bitmask having `SyncLane` and `InputContinuousLane`
const merged = SyncLane | InputContinuousLane; 
console.log(merged); // 0b0000000000000000000000000000101

// Check if bitmask includes the target lane with `&`
console.log((merged & InputContinuousLane) != 0); // true

For React, which handles a massive number of flags, managing them with a single bitmask is likely more efficient than managing each flag with individual variables.

Lanes are determined by the event source

By the way, how are lanes determined?
The most straightforward pattern is that they are determined by the source of events, such as click events.

Let's consider the following code.
When the button is clicked, the text inside the button changes from "Click" to "Clicked."

const App = () => {
  const [test, setTest] = React.useState('Click')

  const onClick = () => {
    setTest('Clicked')
  }

  return React.createElement('button', { onClick }, test);
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App, {}, null));

In fact, to determine where the event originated, React calls a function called listenToAllSupportedEvents during the initial rendering, adding a large number of event listeners to the root element in advance. You can also confirm this in DevTool.

When you click the button, before the onClick process begins, the above event listener is triggered, and the priority corresponding to the event, i.e., the lane, is set. In the case of this click event, SyncLane is set. This means that the click event is treated as having quite high priority.

This is likely from the perspective that it is natural for the reaction to return immediately when a user clicks. On the other hand, mouse move events become InputContinuousLane. This has a slightly lower priority than a click event.

Peek at the actual code

I have summarized the list of lanes and in what kind of events they are used below.
Items for which I could not identify the usage location are listed as "Unknown."

Lane Name Where it is used
NoLane Used as the initial value for a lane.
SyncLane Used for Click events, MouseDown events, etc.
InputContinuousHydrationLane Unknown. Used when combining Suspense and Hydration?
InputContinuousLane Used for MouseEnter events, etc.
DefaultHydrationLane Unknown. Used when combining Suspense and Hydration?
DefaultLane Mainly used during initial rendering.
TransitionHydrationLane Unknown. Used when combining Suspense and Hydration?
TransitionLane1~16 Used in startTransition. The number changes each time it is used, and returns to 0 after using up to 16.
RetryLane1~5 Used when Suspense is still loading. The number changes each time it is used, and returns to 0 after using up to 5.
IdleHydrationLane Unknown. Used when combining Suspense and Hydration?
IdleLane Unknown.
OffscreenLane Unknown. Used in the Offscreen feature scheduled to be implemented in the future?[2]

What are lanes used for?

So, what are lanes used for? They are used all over the place, and I could not understand everything, but there are two prominent ways they are used, which occur during rendering:

  • Assigning lane flags to individual Fibers to determine whether a Fiber needs work during an update rendering, or whether it can be skipped.
  • Determining whether the "Render phase" can be interrupted (which I introduced earlier).

Re-examining the interruptibility of the "Render phase"

Based on the explanation above, let's look at the includesBlockingLane function, which determines the interruptibility of the "Render phase," again. The following lanes could not be interrupted:

Uninterruptible Lanes

  • InputContinuousHydrationLane
  • InputContinuousLane
  • DefaultHydrationLane
  • DefaultLane

What remains (those that might be interrupted) are the following lanes:

Lanes that might be interrupted

  • NoLane
  • SyncLane
  • TransitionHydrationLane
  • TransitionLane1~16
  • RetryLane1~5
  • IdleHydrationLane
  • IdleLane
  • OffscreenLane

NoLane is the default value, so it probably never reaches the includesBlockingLane function. Ignoring OffscreenLane as it is scheduled for implementation, IdleLane and IdleHydrationLane are slightly suspicious, but given the name "Idle," I believe they do nothing, so I trust they do not reach the judgment point (wishful thinking). And SyncLane actually receives special treatment, as the "Render phase" is triggered via a completely different path (which I will describe later in the scheduling section), so it is also excluded from being an interruptible lane.

What remains are TransitionHydrationLane, TransitionLane1~16 (used in startTransition), and RetryLane1~5 (used in Suspense), so it seems correct that only startTransition and Suspense may cause the "Render phase" to be interrupted.

About Scheduling

We have learned that the "Render phase" may be interrupted depending on the lane. Then, how is the "Render phase" triggered in the first place?

React has a task scheduling function, and the "Render phase" is queued as a task and then executed with a delay. The timing for queuing is mainly during "initial rendering" or "event occurrence" (such as button clicks). As mentioned earlier, SyncLane receives special treatment, and the path that calls the "Render phase" is different. Let's look at cases other than SyncLane in order.

Case 1: Not SyncLane

1. Adding the task to a binary heap

Let's peek at the case where it is not SyncLane. After "initial rendering" or an "event" (such as a button click) occurs, a task is generated to execute the "Render phase." The task has the following object structure:

var newTask = {
  id: // Task ID. Tasks with the same sortIndex are sorted by ID,
  callback: // Function that executes the actual work. Here, the function that starts the "Render phase",
  priorityLevel: // Priority derived from the lane (what it is used for is unknown),
  startTime: // Start time,
  expirationTime: // Time until timeout. The higher the lane priority, the shorter this is. In the case of a rendering task, if it times out, it is executed without interrupting the "Render phase",
  sortIndex: // Value used for sorting. Usually, the same value as expirationTime is entered (for delayed tasks, startTime is entered)
};

This task is added to the array for task management that React holds internally. This array is composed of a binary heap.

When adding a value to a binary heap, it is rearranged so that the beginning of the array is the task with the highest priority. Priority is determined based on the sortIndex value. (If the sortIndex is the same, id is used.)

Since we want to process tasks starting from the highest priority, a binary heap, which allows the highest priority task to be retrieved just by looking at the beginning of the array, is efficient.

2. Adding the function that processes the task as a macrotask

After storing it in the binary heap, it does not immediately start the task work. A function called flushWork, which retrieves and processes the task, is queued and executed with a delay. The "queue" referred to here is not a unique React implementation. It is a concept called "macrotask" or "microtask" that the browser's JavaScript originally possesses.

Both behave like queues, but they handle different events. "Macrotasks" mainly process events like setTimeout and mousemove, while "microtasks" handle operations like Promise. (For more detailed information, please refer to articles such as javascript.info.)

Which queue to use, "macrotask" or "microtask," is determined by whether or not the current lane is SyncLane. If the current lane is SyncLane, it is queued in a "microtask"; otherwise, it is queued in a "macrotask." "Microtasks" tend to be executed in preference to "macrotasks." Tasks with SyncLane are used for click events and character input events, as mentioned. Because it is desirable for the user to get an immediate reaction to such events, they adopt "microtasks."

Since we are looking at a case that is not SyncLane this time, it is queued as a "macrotask."

3. The flushWork function is called via the event loop

Afterward, the flushWork function queued as a "macrotask" is called via the event loop. In the flushWork function, one task is retrieved from the binary heap mentioned earlier, and the task's callback function (in this case, the function that starts the Render phase) is executed, initiating the Render phase.

I have summarized the flow up to this point in the figure below.

Peek at the actual code

When a user interaction event occurs, the unstable_scheduleCallback function is called.
After the task is pushed onto the stack within the function, requestHostCallback(flushWork); adds the flushWork function as a macrotask.
https://github.com/facebook/react/blob/9e3b772b8cabbd8cadc7522ebe3dde3279e79d9e/packages/scheduler/src/forks/Scheduler.js#L308-L388

Inside the flushWork function, it calls the workLoop function.
https://github.com/facebook/react/blob/9e3b772b8cabbd8cadc7522ebe3dde3279e79d9e/packages/scheduler/src/forks/Scheduler.js#L147-L187

Inside the workLoop function, it retrieves a task from the binary heap and executes the task's callback function.
https://github.com/facebook/react/blob/9e3b772b8cabbd8cadc7522ebe3dde3279e79d9e/packages/scheduler/src/forks/Scheduler.js#L189-L244

Case 2: SyncLane

1. Adding the task to an array

In the case of SyncLane, a function that executes the "Render phase" is added to a normal array called syncQueue rather than a binary heap. The reason for not using a binary heap is not certain, but it is likely because no tasks other than SyncLane are queued in syncQueue, and priority is irrelevant.

2. Adding the function that processes the task as a microtask

This time, a function called flushSyncCallbacks is added to the queue as a "microtask." This function is one that executes all tasks in the syncQueue array.

3. The flushSyncCallbacks function is called via the event loop

Afterward, the flushSyncCallbacks function queued as a "microtask" is called via the event loop. Inside the flushSyncCallbacks function, all functions in the syncQueue array are executed.
Since the functions that execute the "Render phase" are queued in the syncQueue, the "Render phase" will begin.

The flow in the case of SyncLane is as follows:

Following the Flow of Updates

We have now covered the functionalities of the "Render phase," "Commit phase," "Lanes," and "Scheduling." With that foundation, let's trace the flow of an update. We will also introduce the data structure of useState along the way. We will follow this using the sample code below.

Since an update requires some kind of hook and event to reproduce, we have prepared an onMouseMove. When you move your mouse cursor over the <div>, the text "Helloworld" will change to "ABCworld".

<html>
  <body>
    <script src="./node_modules/react/umd/react.development.js"></script>
    <script src="./node_modules/react-dom/umd/react-dom.development.js"></script>

    <div id="root"><!-- React result will be injected here --></div>

    <script>
      const App = () => {
        const [test, setTest] = React.useState('')

        const onMouseMove = () => {
          setTest((test) => 'A')
          setTest((test) => test + 'B')
          setTest((test) => test + 'C')
        }

        const child = (test === '') ?
          React.createElement('span', {}, 'Hello') :
          React.createElement('b', {}, test)

        return React.createElement('div', { onMouseMove },
          child,
          React.createElement('span', {}, 'world')
        );
      }

      const root = ReactDOM.createRoot(document.getElementById('root'));
      root.render(React.createElement(App, {}, null));
    </script>
  </body>
</html>

We will proceed assuming the initial rendering is already finished, leaving the Fiber tree in the following state:

Let's see what happens when you move the mouse cursor over the <div>.

1. Creating a circular linked list from hooks

When the mouse cursor moves over the element, the onMouseMove event in the sample code is triggered.

const onMouseMove = () => {
  setTest((test) => 'A')
  setTest((test) => test + 'B')
  setTest((test) => test + 'C')
}

setTest is the return value of the useState function, used to change the state. Let's see what is happening internally.

Calling setTest executes a function called dispatchSetState, which generates an update object like the one below.

var update = {
  lane: lane,  // Current lane, which is `InputContinuousLane` in this example
  action: action, // The `onMouseMove` function is passed here
  hasEagerState: false, // Omitted as it is for efficiency
  eagerState: null, // Omitted as it is for efficiency
  next: null // Reference to the next `update` object
};

This update object is managed by a circular linked list as shown below. Each time setTest is called, an update object is added to the tail of the circular linked list. In this case, the completed list looks like this:

This circular linked list is saved in memoizedState of the Fiber that owns the hook. The final state ("ABC") is not calculated at this timing.

2. Scheduled and the "Render phase" begins

Afterward, as explained in the scheduling section, a macrotask that starts the "Render phase" is queued. The event loop executes the task at an appropriate time, and the "Render phase" begins.

3. Copying existing Fibers to leverage them

Unlike the initial rendering, a rendering has already been completed once. Therefore, a Fiber tree already exists within React. During updates, we leverage this.

Where do the changes occur? Moving the mouse cursor over the element changes <div style="white-space:nowrap"><span>Hello</span><span>world</span></div> to <div style="white-space:nowrap"><b>ABC</b><span>world</span></div>. In other words, the following tasks are necessary:

  • Remove the <span>Hello</span> element
  • Add the <b>ABC</b> element

Resolving where changes should be made like this is called "Reconciliation[3]."

However, it is important to note that no changes are made to the actual DOM during the "Render phase." As previously mentioned, the "Render phase" may be interrupted midway. If we were to change the actual DOM while interrupted, a partially updated state might be displayed on the screen. Therefore, in the "Render phase," we simply note the changes on the Fiber and apply the changes to the actual DOM in the subsequent "Commit phase."

Here, the "double buffering" technique comes into play. Instead of rebuilding the Fiber tree from scratch, we reconstruct it by copying the existing Fiber tree. For clarity, I have illustrated the current working Fiber tree as "WIP (workInProgress)" and the currently displayed Fiber tree as "Current."

Note that the "copy" mentioned here refers to a "shallow copy." Briefly, while the Fiber itself is a different object, properties under the Fiber, such as stateNode and memoizedState, are shared objects between the source and the copy. (Due to space limitations, memoizedState is omitted from the figure below.)

Since the stateNode was already added to div#root via appendChild during the initial rendering, rewriting the DOM of the stateNode on the WIP side would reflect changes on the screen. To avoid this, actual DOM deletion and modification work is left until the "Commit phase."

4. Adding a new Fiber

Next, we execute the circular linked list saved in memoizedState to retrieve the new state string, "ABC". We create and add a new Fiber for <b>ABC</b> from there.
Since this Fiber is new rather than a copy, its stateNode has not been added under the div#root DOM, so creating a stateNode at this timing will not display anything on the screen.

5. Copying Fibers with no changes

As in step 3, Fibers with no changes are replicated via shallow copy. We shallow copy <span>world</span>.

6. Marking Fibers to be deleted

Next, we remember the Fiber that must be deleted. In this case, it was <span>Hello</span>. We store it in the deletion array held by the parent <div> Fiber. (Click on the image to enlarge if it is difficult to see.)

With this, the "Render phase" concludes, and we proceed to the "Commit phase."

7. Executing DOM updates in the "Commit phase"

In the "Commit phase," we perform the actual DOM reconstruction using the WIP Fiber and the deletion array prepared earlier. We traverse the tree from the root (HostRoot) down through the child elements and apply modifications, deletions, and additions. First, we use removeChild to remove the DOM of <span>Hello</span>, referenced in the deletion array, from the parent Fiber's stateNode (marked in red).

At this timing, only the text "world" is visible on the screen (though it is too fast for the user to perceive...).

Next, we use insertBefore to add the new <b>ABC</b> element to the parent Fiber's stateNode.

With this, the DOM update is complete.

8. Setting the WIP as the new Current

Since the changes are now finalized, we set the current "WIP" Fiber tree as "Current" for the next rendering. The old "Current" Fiber tree is no longer needed. We modify the current property of the "FiberRootNode" to point to the HostRoot on the WIP side.

With this, the rendering is complete.

Conclusion

As we have seen, the internal workings of React are quite complex.

It is not a logic that can be understood intuitively, but as stated in the Design Principles, it is interesting to think that it has become such a popular library precisely because it prioritized efficiency and development speed over elegance.

Reference Articles

I studied using the following articles to read the React code. Thank you.

脚注
  1. https://ja.reactjs.org/docs/react-api.html#createelement ↩︎

  2. https://jser.dev/react/2022/04/17/offscreen-component.html#reconciling-offscreen-component ↩︎

  3. https://reactjs.org/docs/reconciliation.html ↩︎

Discussion