DEV Community

Cover image for How does React 18 work inside?
ktmouk
ktmouk

Posted on

How does React 18 work inside?

I recently read the React18 source code on GitHub and learned some exciting things. In this post, I want to describe how React18 works, by using figures.

I have also included GitHub links in some sections for readers who want to look at the code.

Fiber

First of all, I will explain what Fiber is. If you are interested in React, you may have heard the word "Fiber" somewhere. However, there are few documents and articles that explain Fiber.

What is Fiber?

In brief, one of the Fibers means one of the components, such as <MyComponent> and <div>. React builds a Fiber tree (it's like a DOM tree) for calculating where it has changed due to user interactions.

Besides, Fiber also means a unit of the task of the queue. React can pause the rendering per Fiber.

Why does React use the Fiber tree?

This is because React can more easily manage the data associated with a component, such as the priority, the component name, etc., than using a DOM tree.

What is the structure of the Fiber tree?

Let's see what the structure of the Fiber tree is, I have prepared a sample code below to describe it.

<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 will inject elements 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>
Enter fullscreen mode Exit fullscreen mode

If you run the code, React will create the Fiber tree like this.

The graph of Fiber tree

As you can see, This tree is almost the same as a DOM tree.
However, it has some differences from a DOM tree, like the one below.

  • The Fiber tree contains custom components such as <App>.
  • There are HostRoot and FiberRootNode components in the tree.

So, what are HostRoot and FiberNode?

FiberNode

FiberNode is the node indicated by the green circle in the figure. It has some properties named child, sibling, and return. These properties are for accessing its child, sibling, and parent.

FiberRootNode

FiberRootNode is the node indicated by the red circle in the figure. Its structure is entirely different from FiberNode, and it has a property called containerInfo that allows React to access the root element by accessing this property.

How does React create the Fiber tree?

Then, let's see how React creates the Fiber-tree.
I describe the rendering process using the sample code below and have added comments at key points in the code.

<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 will inject elements here --></div>

    <script>
      const App = () => {
        const child = React.createElement('span', null, 'world');
        return React.createElement('div', null, 'Hello', child);
      }
      // ★1 Create the FiberRootNode and the HostRoot
      const root = ReactDOM.createRoot(
        document.getElementById('root')
      );  

      // ★2 Create the ReactElement of `<App>`
      reactElement = React.createElement(App, {}, null)

      // ★3 Start render process
      root.render(reactElement);  
    </script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

★1 Create the FiberRootNode and the HostRoot

When React calls the ReactDOM.createRoot method, it invokes the function named createFiberRoot to create the FiberRootNode and the HostRoot.

You can look at the code on GitHub.

After creating the FiberRootNode and the HostRoot, the Fiber tree looks like this.

The Fiber tree having the FiberRootNode and the HostRoot

★2 Create the ReactElement of <App>

Then, React creates the ReactElement, which is required for calling the root.render function.

Note that this object structure is completely different from Fiber. If you use the JSX format, you typically don't need to use the React.createElement method because a compiler like Webpack will convert JSX to the React.createElement at compile time. (docs)

★3 Start rendering

Finally, the root.render function is called, and React starts rendering. This process has three steps.

  1. React recursively converts ReactElements to Fibers and adds them to the Fiber tree until it reaches the terminal node.
  2. When React reaches the terminal node, it returns to the parent while saving a DOM element which is made from a Fiber to the property named stateNode.
  3. In the end, when React reaches the root, it appends the completed DOM elements to div#root with appendChild.

The explanation is complicated, so I made a GIF image to make it easier to understand.

The stateNode property generally holds the DOM element, including itself and its children, but functional components (such as <App>) don't have stateNode. Their stateNode property is always null.

The gif explaining the flow of creating a fiber tree

Finally, the function finds the closest stateNode (except for null) from the root and appends it to the div#root. Then, a user can see the message "Helloworld" on the screen.

Appending the stateNode to the root

Rendering phase and Commit phase

I explained the flow of the rendering process in the previous section. In this section, I will explain the rendering process in more detail.

Two phases in the rendering process

The rendering process can be divided into two phases, the rendering phase, and the commit phase.

Two phases

In the rendering phase, React converts ReactElements to DOM elements and stores them in stateNode. Then, in the commit phase, React appends the DOM elements to div#root.

It means React doesn't change DOM elements during the rendering phase, and any changes are never shown on the screen until the commit phase starts. So, React can pause or resume the rendering phase anytime if it takes longer.

This behavior provides a good user experience because React doesn't block the JavaScript thread for long. JavaScript is a single-threaded language, and if React blocks the thread for too long, the user may think the browser has frozen up.

On the other hand, in the commit phase, React can block the JavaScript thread. This is not good for the user experience. However, React just updates DOM elements based on the Fiber tree in the commit phase, so React completes its tasks quickly and releases the thread.

In what cases does React pause the rendering phase?

I was curious about this and looked into cases of the rendering phase can be paused. According to this issue below, React can only pause the rendering phase only if we use startTransition or Suspense.

Bug: time slice not work in react 18 #24392

in react@16.8.0, a long task will be sliced multi short task, demo: https://stackblitz.com/edit/react-ts-aqwejz image

but in react@18.0.0, it will be only a long task, demo: https://stackblitz.com/edit/react-ts-ezgtzn image

Is it a react18 time slice bug or feature?

React version: 16.8.0 & 18.0.0

Steps To Reproduce

  1. run a example app like below
  2. open inspector -> performance, then record and analyze

Link to code example: 16.8.0: https://stackblitz.com/edit/react-ts-aqwejz 18.0.0: https://stackblitz.com/edit/react-ts-ezgtzn

The current behavior

in react@18.0.0, long task not be sliced

The expected behavior

in react@18.0.0, long task will be sliced

So if you want to allow React to pause the render phases, you need to wrap your code with startTransition or Suspense, like this.

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

Then, How does React start the rendering phase inside?
React calls the workLoopConcurrent or workLoopSync function to start the rendering phase. Both functions start the rendering phase, but workLoopConcurrent can pause it, and workLoopSync cannot.

The workLoopConcurrent function calls the shouldYield function to check whether React should pause the rendering phase. The shouldYield function returns true if more than 5ms has passed since the phase start.

You can look at the code on GitHub.

How does React decide which function to call? It depends on Lane.

What is Lane?

Briefly, Lane is a 32-bit mask flag. Each bit represents a priority and a type of task. The closer the lane is to '0', the higher the priority.

The list of Lane

Also, the NoLane is a special one. Basically, this Lane is for a default value.

You can take a look at the list of lanes below.
ReactFiberLane.old.js (GitHub)

As you can see, the lane is just an integer. React can use bitwise operators to create a bitmask containing some lanes or check if the bitmask has a particular lane.

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

// Merge lanes by using `|` operator
// Returns a bitmask containing `SyncLane` and `InputContinuousLane`.
const merged = SyncLane | InputContinuousLane; 
console.log(merged); // 0b0000000000000000000000000000101

// Check if the bitmask have `InputContinuousLane`
console.log((merged & InputContinuousLane) != 0); // true
Enter fullscreen mode Exit fullscreen mode

In the rendering process, React has to handle a lot of flags. The bitmask is a good solution for managing flags, compared to having them with individual variables.

How does React choose lanes?

There are several ways that React can select a lane, but one of the simplest is to select a lane based on an event type, such as a click event.

Let's look at the sample code below. When you click the button, the text in 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));
Enter fullscreen mode Exit fullscreen mode

After the page loaded, React calls the listenToAllSupportedEvents function to add many event listeners, allowing React to catch almost all user events. You can look at these listeners in the DevTools.

Many event listeners

When you click a button, the above event listener is fired before the onClick callback is triggered, and React decides the lane based on an event type. In the case of the sample code, React selects the SyncLane when a click event occurs. It means that the click event is a high priority.

This is because a user generally wants the browser to respond quickly when the user clicks something. On the other hand, a mouse-move event is not higher than a click event.

You can look at the code on GitHub.

I looked at the usage of lanes in the source code and made a list. I put "unknown" in the place where I couldn't find the usage.

Name Usage
NoLane Default value
SyncLane ClickEvent, MouseMoveEvent, etc.
InputContinuousHydrationLane Unknown. It is probably used for Suspense and hydrateroot
InputContinuousLane MouseEnter, etc.
DefaultHydrationLane Unknown. It is probably used for Suspense and hydrateroot
DefaultLane It is mainly used for initial rendering.
TransitionHydrationLane Unknown. It is probably used for Suspense and hydrateroot
TransitionLane1~16 It is used for startTransition. React uses a different number each time, and if React previously used TransitionLane16, it will use TransitionLane1.
RetryLane1~5 React uses it when Suspense is still loading.
IdleHydrationLane Unknown. It is probably used for Suspense and hydrateroot
IdleLane Unknown
OffscreenLane Unknown. It's probably used for the offscreen feature, which is not implemented yet.

How does React use lanes?

So how does React use lanes? It is not easy to list all the uses, because React uses them in many different ways, but the main uses are as follows.

  • React uses the lanes contained in each Fiber to determine if a Fiber has any updates and should be re-rendered.
  • React uses a lane to decide if it can pause the rendering phase. (This is what I described in the previous section.)

About the scheduling

In the previous section, I described the rendering phase and the lane. In this section, I will explain the scheduling that React has.

React has a scheduling system. The scheduler queues the rendering phase as one of the tasks and invokes it with a delay. The scheduler queues the rendering phase when the initial load or any event occurs, such as a click event.

And interestingly, SyncLine (used for click events, etc.) is special because React chooses a scheduler system based on whether the current lane is SyncLine or not.

Let's see what the difference is between SyncLine and other lanes.

Case 1. non-SyncLane

1. Create and queue a task.

After any events or an initial page load happens, React creates a task object like the one below.

var newTask = {
  id: // The task id. It is used to sort tasks.
  callback: // This property holds the function called when the task is dequeued, such as the function to start the rendering process.
  priorityLevel: // Unknown.
  startTime: // The time this task was performed.
  expirationTime: // The expiration time. The higher the lane priority, the shorter it is. When it expires, React executes it without pausing.
  sortIndex: // The index for sorting tasks. Basically, its value is the same as startTime. If different tasks have the same sortIndex, React will use `id` instead.
};
Enter fullscreen mode Exit fullscreen mode

Then, React pushes it into the array for managing tasks. This array consists of a binary heap.

As a feature of the binary heap, the head of the array is always a high-priority task. React uses the sortIndex property as a priority, and sorts tasks based on the sortIndex.

How does React sorts tasks?

A binary heap is an ideal structure for managing tasks because React can pick the highest priority task by simply accessing the head of an array.

2. Add the flushWork function to the macrotask queue.

After adding the task to a binary heap, React doesn't immediately execute it. React executes the task via the flushWork function. This function is called through the macrotask queue.

Note that the macrotask queue is not the concept of React. this queue is implemented by JavaScript. JavaScript has two queues, MacroTask and MicroTask. Both have some sort of queue structure, but these roles are completely different.

MacroTask is used to execute an event callback, such as setTimeout mousemove, and MicroTask is used to execute Promise callback. For more information, see javaScript.info.

React uses MacroTask and MicroTask depending on whether the current lane is SyncLine or not. If the current lane is SyncLane, React uses MicroTask to perform the flushWork function. Otherwise, React will use MacroTask instead.

This is because JavaScript generally executes the MicroTask before the MacroTask, and SyncLane is used for an important event like a click event, so React needs to complete the SyncLane task as quickly as possible for user experience.

In this section, we are looking at the case of non-SyncLane, so React uses the MacroTask.

3. The flushWork function is called via EventLoop.

Then, the flushWork function is called via EventLoop. In the flushWork function, React picks the highest task from the binary heap and executes it (in many cases, this task is to start the rendering phase).

The above process is summarized in the figure below.

The flow of processing the task that has non-SyncLane

Case 2. SyncLane

1. Add the task to the array.

If the current lane is SyncLane, React adds the task to the array named syncQueue. This array is not the binary heap structure and doesn't need to be prioritized. This is because this array is only used for SyncLane tasks, not for tasks from other lanes.

2. Add the flushSyncCallbacks function to the MicroTask queue.

Then, React queues the function named flushSyncCallbacks to the MicroTask queue, not the MacroTask queue. This function will execute all of the tasks in the syncQueue array in a row.

3. The flushSyncCallbacks function is called via EventLoop.

Then, the flushSyncCallbacks function is called via EventLoop, and all of the tasks in the syncQueue array are executed.

The above flow is shown in the figure below.

The flow of processing the task that has SyncLane

How does React updates the components?

In the previous sections, I have explained the rendering phase, commit phases, lanes, and scheduling. In this section, I will describe how React updates the components.

I use the following sample code to describe the update flow. This sample code has hooks because it is required to fire any events.

When you hover over the button, a browser will change the text from 'Helloworld' 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 will inject elements 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>
Enter fullscreen mode Exit fullscreen mode

Let's look at the update flow after the initial render. The following fiber tree has already been created in the initial render.

A Fiber tree at the end of the initial render

What happens when you move the mouse over the <div>?

1. Create a circular list of hooks.

When you hover the <div>, the onMouseMove function in the sample code is called.

const onMouseMove = () => {
  setTest((test) => 'A')
  setTest((test) => test + 'B')
  setTest((test) => test + 'C')
}
Enter fullscreen mode Exit fullscreen mode

The setTest function is the return value of the useState function and is used to update the state. So let's see what the setTest function does inside.

When setTest function is called, it calls the dispatchSetState function, and then, it creates the object named update.

var update = {
  lane: lane,  // This property holds the lane. In the case of the sample code, the lane is `InputContinuousLane'.
  action: action, // This property holds the `onMouseMove` function.
  hasEagerState: false, // It is used to improve performance.
  eagerState: null, // It is used to improve performance.
  next: null // The property has a reference to the next `update` object.
};
Enter fullscreen mode Exit fullscreen mode

These update objects consist of a circular list. React adds an update object to the end of the circular list each time setTest is called. In the example, React eventually makes the circular list look like this.

The circular list

Then, React sets this circular list to the memoizedState of the Fiber that owns this hook. Also, React doesn't calculate the result (ABC) at this point.

React sets this circular list to the  raw `memoizedState` endraw

2. The rendering phase is called via EventLoop.

Then, as the previous section described, React pushes the task to the MacroTask queue to start the render phase. After that, EventLoop invokes the task and starts the render phase.

3. Copy and reuse the previous Fiber tree.

The difference from the initial rendering is that React already has a Fiber tree. React reuses the previous tree as much as possible instead of creating a new one.

So, where does React have to change? In the example code case, React has to change the <div><span>Hello</span><span>world</span></div> element to <div><b>ABC</b><span>world</span></div>.
It means, React needs to do the following:

  1. Remove the <span>Hello</span> element.
  2. Add the <b>ABC</b> element.

In the React world, we call this process Reconciliation.

However, React cannot change the real DOM at the render phase because the render phase could be paused for some reason. If React changes the DOM and the render phase is paused, the user will see the website which may be incomplete.

So, React stores the data in the Fiber instead, and updates the DOM during the commit phase.

This is the time to use a technique called "Double Buffering". React copies the previous Fiber tree instead of creating it from the scratch. In the figure below, the Fiber tree that is currently being worked on is labeled "WIP (work in progress)", and the Fiber tree that is currently displayed is labeled as "Current".

Copy Fibers to WIP

Note that JavaScript's "copy" basically means "shallow copy". React copies a Fiber, but not its property. The properties of Fiber refer to the same value as the original Fiber. For example, in the figure below, stateNode and memoizedState are relevant (The memoizedState is omitted due to space limitations.)

Share same property

Also, React cannot update the stateNodes (DOM) at this point because all stateNodes are already appended to div#root via the root element and any update of stateNodes will be rendered immediately on the screen.

4. Appends new Fibers

Then, React executes the circular list that is in memoizedState property, and gains a new state, ABC.
React then creates a new Fiber that has the <b>ABC</B> element, and appends it to the WIP tree.

And React can create the stateNode for the new Fiber at this point. This is not a problem because this Fiber is new and is not appended to div#root, so a browser will not render this stateNode.

Appends new Fibers

5. Copy unchanged Fibers

As same as 3. step, React copies the unchanged Fibers. In the below figure, React copies the <span>world</span> element to WIP.

Copy unchanged Fibers

6. Mark the Fiber to be deleted.

React marks Fibers that should be deleted during the commit phase. In this example, it's <span>Hello</span>. React stores a reference to this fiber in a parent property named deletion, an array.

Mark the Fiber to be deleted

This is the end of the render phase. In the next section, we will look at the commit phase.

7. Update and delete DOM elements in the commit phase

In the commit phase, React updates and deletes elements in the DOM tree based on the Fiber tree and the deletion array. First, React deletes the <span>Hello</span> element with removeChild from stateNode (written in red in the figure) of the parent Fiber.

Delete an element

Then, React inserts a new DOM element, <b>ABC</b>, into the stateNode of the parent Fiber by using insertBefore.

Insert a new element

The user will then see the new text "ABCworld" on the screen.

8. Makes the WIP tree the current tree.

For the next update, React sets the WIP tree to the current property of the FiberRootNode as the current tree. The old "current tree" will no longer be used.

Makes the WIP tree the current tree.

That's all. The rendering is now complete.

Conclusion

React works with complex algorithms and structures and these are hard to understand. but, according to principles, React prioritizes performance and a good developer experience instead of being elegant. So I think this is why most developers love React and prefer to use it.

Latest comments (1)

Collapse
 
fahaxiki profile image
Fahaxiki

bro, you learn the React just read the source code?