January 12

Detailed React. Reconciliation, Renderers, Fiber, Virtual Tree

On the web, you can find a huge number of articles, manuals and books on the basics of React, including official documentation from the developers. In this article, we will not write Hello World and study well-known concepts. Today we will look under the hood of the library and try to understand how it works inside, how it stores data, and what its Virtual Tree actually looks like.

At the time of writing, the latest stable version of React is 18.2.0. Over 10 years, Facebook developers have done a tremendous amount of work, many features have been implemented and not a few optimizations have been made. Over the years, there have also been drastic architectural changes. Obviously, one article is not enough to cover the entire mechanics of React, therefore, this publication will be the first in a series of articles about the internal structure of React. Here we will get acquainted with the basic entities and architectural solutions.

Entry point

First, let's figure out what is the trigger for the React machine. In other words, we will find the entry point. In the case of React, this is not difficult.

Still, you can't do without examples. Let's take a look at the typical React code for a Web application.

import ReactDOM from 'react-dom/client';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(<div>My React App</div>);

This code uses the react-dom/client package to create a Root container and, further, render a DIV element into this container. This is where the React launcher is located, or rather, there are two of them at once: creating a container using createRoot, and starting the rendering process in the container. But about everything in order.

Reconciliation

As we know from open sources, before the changes get into the DOM, React first makes all the necessary modifications in the so-called Virtual Tree. After that, this Virtual Tree "gets" into the real DOM. The process of matching a virtual tree with a real DOM is called reconciliation.

The additional complexity of the process is created by the fact that today there are different platforms where the final UI can be displayed (on the screen or, for example, in a string or file). In particular, React itself provides Web rendering, server rendering (SSR), rendering on mobile devices (React Native), etc.

In this regard, the React team has allocated a separate react-reconciler package as a kind of abstract API. It works in two modes:

  • Mutation mode - for platforms that allow mutating the resulting tree (i.e. have methods similar to appendChild/removeChild)
  • Persistent mode - for platforms with immutable trees. In this case, for each change, the entire tree is cloned, modifications are made, and then the entire tree is completely replaced with the modified one

This package itself does not provide the final binding to the DOM, but only provides all the necessary mechanics for preparing and manipulating elements. The very same direct binding to the DOM is carried out by means of an external provider implementing the react-reconciler package API. The implementation by the provider consists in setting specific config flags and describing callback methods, such as createInstance, appendChild, removeChild, etc.. This approach allows you to create different providers for different cases and platforms.

Renderers

Providers implementing the react-reconciler package API are conventionally called renderers. Theoretically, the react-reconciler package was conceived as a full-fledged API, but since its creation in 2017, work on it has been quite active, significant changes occur periodically, therefore, formally, the package is considered unstable, it is worth using it directly in your projects with caution.

React itself offers several renderer implementations "out of the box".

  • React DOM - we have already seen this renderer in the example above. It provides binding to the browser's DOM tree
  • React Native - this renderer provides native rendering on mobile platforms
  • React ART - allows you to draw vector graphics using React tools. In fact, it is a reactive wrapper for the ART library.

Fiber

Before moving on, it's important to get to know the basic essence of the React engine.

Fiber is an internal React object that represents a task ("work") that the engine has scheduled for execution or has already completed.

Based on these objects, the react-reconciler package, which we talked about just above, will work.

/packages/react-reconciler/src/ReactInternalTypes.js#L67

// A Fiber is work on a Component that needs to be done or was done. There can
// be more than one per component.
export type Fiber = {
  // These first fields are conceptually members of an Instance. This used to
  // be split into a separate type and intersected with the other Fiber fields,
  // but until Flow fixes its intersection bugs, we've merged them into a
  // single type.
  
  // An Instance is shared between all versions of a component. We can easily
  // break this out into a separate object to avoid copying so much to the
  // alternate versions of the tree. We put this on a single object for now to
  // minimize the number of objects created during the initial render.
  
  // Tag identifying the type of fiber.
  tag: WorkTag,
  
  // Unique identifier of this child.
  key: null | string,
  
  // The value of element.type which is used to preserve the identity during
  // reconciliation of this child.
  elementType: any,
  
  // The resolved function/class/ associated with this fiber.
  type: any,
  
  // The local state associated with this fiber.
  stateNode: any,
  
  // Conceptual aliases
  // parent : Instance -> return The parent happens to be the same as the
  // return fiber since we've merged the fiber and instance.
  
  // Remaining fields belong to Fiber
  
  // The Fiber to return to after finishing processing this one.
  // This is effectively the parent, but there can be multiple parents (two)
  // so this is only the parent of the thing we're currently processing.
  // It is conceptually the same as the return address of a stack frame.
  return: Fiber | null,
  
  // Singly Linked List Tree Structure.
  child: Fiber | null,
  sibling: Fiber | null,
  index: number,
  
  // The ref last used to attach this node.
  // I'll avoid adding an owner field for prod and model that as functions.
  ref:
    | null
    | (((handle: mixed) => void) & {_stringRef: ?string, ...})
    | RefObject,
    
  refCleanup: null | (() => void),
    
  // Input is the data coming into process this fiber. Arguments. Props.
  pendingProps: any, // This type will be more specific once we overload the tag.
  memoizedProps: any, // The props used to create the output.
  
  // A queue of state updates and callbacks.
  updateQueue: mixed,
  
  // The state used to create the output
  memoizedState: any,
  
  // Dependencies (contexts, events) for this fiber, if it has any
  dependencies: Dependencies | null,
  
  // Bitfield that describes properties about the fiber and its subtree. E.g.
  // the ConcurrentMode flag indicates whether the subtree should be async-by-
  // default. When a fiber is created, it inherits the mode of its
  // parent. Additional flags can be set at creation time, but after that the
  // value should remain unchanged throughout the fiber's lifetime, particularly
  // before its child fibers are created.
  mode: TypeOfMode,
  
  // Effect
  flags: Flags,
  subtreeFlags: Flags,
  deletions: Array<Fiber> | null,
  
  // Singly linked list fast path to the next fiber with side-effects.
  nextEffect: Fiber | null,
  
  // The first and last fiber with side-effect within this subtree. This allows
  // us to reuse a slice of the linked list when we reuse the work done within
  // this fiber.
  firstEffect: Fiber | null,
  lastEffect: Fiber | null,
  
  lanes: Lanes,
  childLanes: Lanes,
  
  // This is a pooled version of a Fiber. Every fiber that gets updated will
  // eventually have a pair. There are cases when we can clean up pairs to save
  // memory if we need to.
  alternate: Fiber | null,
  
  // Time spent rendering this Fiber and its descendants for the current update.
  // This tells us how well the tree makes use of sCU for memoization.
  // It is reset to 0 each time we render and only updated when we don't bailout.
  // This field is only set when the enableProfilerTimer flag is enabled.
  actualDuration?: number,
  
  // If the Fiber is currently active in the "render" phase,
  // This marks the time at which the work began.
  // This field is only set when the enableProfilerTimer flag is enabled.
  actualStartTime?: number,
  
  // Duration of the most recent render time for this Fiber.
  // This value is not updated when we bailout for memoization purposes.
  // This field is only set when the enableProfilerTimer flag is enabled.
  selfBaseDuration?: number,
  
  // Sum of base times for all descendants of this Fiber.
  // This value bubbles up during the "complete" phase.
  // This field is only set when the enableProfilerTimer flag is enabled.
  treeBaseDuration?: number,
  
  // Conceptual aliases
  // workInProgress : Fiber ->  alternate The alternate used for reuse happens
  // to be the same as work in progress.
  // __DEV__ only
  
  _debugSource?: Source | null,
  _debugOwner?: Fiber | null,
  _debugIsCurrentlyTiming?: boolean,
  _debugNeedsRemount?: boolean,
  
  // Used to verify that the order of hooks does not change between renders.
  _debugHookTypes?: Array<HookType> | null,
};

As we can see, the object has quite a lot of properties. Within this article we will not be able to disassemble them all, but let's take a look at those that will allow us to understand the mechanics of the engine.

tag

An important parameter indicating the type of entity. At the moment there are 28 of them

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;
export const HostHoistable = 26;
export const HostSingleton = 27;

Initially, during initialization, tag is equal to IndeterminateComponent, because it is still unknown what exactly this Fiber will represent. Later, the tag will be assigned a value corresponding to its essence.

key

The React key that no array (and not only an array) can do without

type

Directly the Fiber type. There may be a link to a component class, a function of a functional component, a tag of a browser element, or, for example, a pointer symbol to a hook, context, or some other effect.

stateNode

A link to the mapped date in the DOM tree.

child, sibling, index

References to the child element, the neighboring element, and the index of this Fiber itself at its own level (if Fiber itself is an element of the array)

These links are needed to implement the Linear Singly Linked List pattern.

ref

The ref property passed to the component

pendingProps, memoizedProps

Snapshots of the props object.

  • memoizedProps - old (current) component props
  • pendingProps - new component props

They will be compared in the future to decide whether the component needs to be redrawn.

memoizedState

The current state of the component. Unlike pendingProps/memoizedProps, the component has no future state, only the current one, because it is not transmitted from outside, but "lives" inside the component. The impression of the next state will be calculated directly during the render phase, and the calculation algorithm may differ depending on whether it is a functional component or a class component.

flags, subtreeFlags

A 32-bit number for Fiber itself and its children, respectively. It is a mask for a set of flags reflecting the phase/state of the current Fiber. When passing through the tree, at each iteration, the engine will check the flags in one way or another and take Fiber to the appropriate algorithmic branch.

deletions

An array of Fibers that need to be removed from the tree.

nextEffect

A link to the next Fiber representing the side-effect (for example, a hook). As in the case of child, it is an implementation of the Linear Singly Linked List pattern.

firstEffect, lastEffect

The first and last Fibers representing the side-effect in this subtree.

lanes, childLanes

A 32-bit number for Fiber itself and its children, respectively. Defines Fiber in some Lane. It is necessary to prioritize tasks. The tasks of some lanes are more prioritized in relation to other lanes, and will be completed first.

alternate

One of the key properties of Fiber. A copy of Fiber itself will be stored here during the render phase. All changes will occur in this copy. The Fiber itself will be changed in the commit phase.

FiberRoot

Additionally, to work with the Root container, React provides a separate type of FiberRoot.

The composition of its properties differs from the composition of Fiber properties, there are much more of them here, for example, properties for working in Suspension mode are presented, as well as properties for the profiler.

/packages/react-reconciler/src/ReactInternalTypes.js#L334

export type FiberRoot = {
  ...BaseFiberRootProperties,
  ...SuspenseCallbackOnlyFiberRootProperties,
  ...UpdaterTrackingOnlyFiberRootProperties,
  ...TransitionTracingOnlyFiberRootProperties,
  ...
};

Of the entire set of FiberRoot properties, today we will be interested in only some

tag

Similar to Fiber, but there can only be of two values

export const LegacyRoot = 0;
export const ConcurrentRoot = 1;

LegacyRoot is an obsolete container creation option. Up to and including React 17, the container was created and rendered by calling a single render method

import ReactDOM from 'react-dom';

ReactDOM.render(<div>My React App</div>, document.getElementById('root'));

As of React 18, this approach is considered deprecated and is not recommended for use. Instead, it is proposed to create a container through createRoot, which creates a container of the ConcurrentRoot type, whose operation is radically different from the old version and, among other things, supports Suspense mode.

current

A dynamic link to the current Fiber in work. The link will change as the react-reconciler package traverses the tree.

pendingLanes, suspendedLanes, pingedLanes, expiredLanes, errorRecoveryDisabledLanes, finishedLanes

Various flags for Lanes used in the rendering process.

Life cycle

Root container creation

Let's go back to our original example

import ReactDOM from 'react-dom/client';

const root = ReactDOM.createRoot(document.getElementById('root'));

By calling the createRoot method of the ReactDOM renderer, we create an instance of FiberRoot

root: FiberRootNode {
  callbackNode: null,
  callbackPriority: 0,
  containerInfo: div#root,
  context: null,
  current: FiberNode {
    actualDuration: 0,
    actualStartTime: -1,
    alternate: null,
    child: null,
    childLanes: 0,
    deletions: null,
    dependencies: null,
    elementType: null,
    flags: 0,
    index: 0,
    key: null,
    lanes: 0,
    memoizedProps: null,
    memoizedState: {
      element: null,
      isDehydrated: false,
      cache: null,
      transitions: null,
      pendingSuspenseBoundaries: null,
    },
    mode: 3,
    pendingProps: null,
    ref: null,
    return: null,
    selfBaseDuration: 0,
    sibling: null,
    stateNode: {/* ref to the FiberRootNode */},
    subtreeFlags: 0,
    tag: 3,
    treeBaseDuration: 0,
    type: null,
    updateQueue: {
      baseState: {
        cache: null,
        element: null,
        isDehydrated: false,
        pendingSuspenseBoundaries: null,
        transitions: null,
      },
      firstBaseUpdate: null,
      lastBaseUpdate: null,
      shared: {
        interleaved: null,
        lanes: 0,
        pending: null,
      },
      effects: null,
    },
    _debugHookTypes: null,
    _debugNeedsRemount: false,
    _debugOwner: null,
    _debugSource: null
  },
  effectDuration: 0,
  entangledLanes: 0,
  entanglements: (31) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  eventTimes: (31) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  expirationTimes: (31) [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
  expiredLanes: 0,
  finishedLanes: 0,
  finishedWork: null,
  identifierPrefix: "",
  memoizedUpdaters: Set(0) {size: 0},
  mutableReadLanes: 0,
  mutableSourceEagerHydrationData: null,
  onRecoverableError: ƒ reportError(),
  passiveEffectDuration: 0,
  pendingChildren: null,
  pendingContext: null,
  pendingLanes: 0,
  pendingUpdatersLaneMap: (31) [Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0), Set(0)],
  pingCache: null,
  pingedLanes: 0,
  suspendedLanes: 0,
  tag: 1,
  timeoutHandle: -1,
  _debugRootType: "createRoot()",
}

An uninitialized FiberNode with tag = 3 (HostRoot) is created along with the container.

At this stage, the temporary cache is also initialized (while the container is not mounted yet).

Root container hydration

Next, we call the render method on the container for the first time

root.render(<div>My React App</div>);

First of all, the reconciler requests the priority Lane for the current "work" (the current, not yet initialized FiberNode stored in .current), which depends on the rendering mode (Suspense/Sync) and whether the element is mounted or not yet.

All subsequent work will be carried out on the FiberNode, as on an abstract Root element, i.e. the reconciler does not need to know whether the node is a container or not, the entire subsequent cycle will be the same for any node that initiates the update.

Next, the context of the React component itself is created. In our example, at this stage, a JSX parser is included in the work, which will parse our HTML-like syntax into ReactComponent. We will not consider the work of JSX in this article. Instead of JSX, we could create an element using React.createElement('div'), this does not affect the reconciliation in any way (apart from the cost of JSX parsing).

The Update Lane is also requested for the received ReactComponent instance. If the component contains other nested components, they will also be determined later on the next ticks and will also fall into the appropriate band.

Next, a link to the current "work" (Fiber) is written to the scheduler, as well as the entire Fiber is queued for updating in a special concurrentQueues stack (for processing possible Suspensions).

The root Fiber (in our case, the Root container) is marked as updated (but not yet completed) by setting a flag

root.pendingLanes |= updateLane

suspendedLanes flags are also set here, if required.

At the end of the process, the scheduled Root nodes updates are checked.

Scheduler

The scheduler handles the queued tasks (Fibers) and always works in a microtask or after the processing of the current event is completed. A specific render has the ability to define its own specifics of the scheduler by overriding the scheduleMicrotask in the react-reconciler API package. For example, React Native can use queueMicrotask for these purposes, if supported by the platform, and React DOM rely on promises.

In our basic example, the scheduler check was started manually by calling the render method. Next, the scheduler will listen to React events and run the check if necessary.

Having found a link to Fiber on the next tick, the scheduler determines the next task based on the current Fiber it has. This happens in the same way, in a separate microtask, in which a number of checks take place, including determining the priority of the next task.

Eventually, when a Fiber component is detected (a Fiber which represents a React component), processing of this Fiber is started.

Fiber processing

The key object in Fiber processing is a copy of the current node stored in Fiber.alternate. The link to this Fiber.alternate is written in FiberRootNore.current. Next, each subsequent (neighboring or child) component will register a reference to its alternate in the current, and all subsequent mechanisms will refer to it as the current object in work. Thus, at the same time, only one component is always in processing, and the rest will stand in line until their turn comes.

Phases

At the moment Fiber processing begins, a whole cycle of operations on the node is launched, which can be conditionally divided into four phases.

Begin phase

This phase is the very initial stage of node processing. The first step here is to determine the branch of the subsequent component lifecycle.

In our example, as we saw earlier, Fiber.alternate is still empty. This tells the recondciler that the component has not yet been mounted (or was unmounted intentionally). In this case, the current task is marked with the didReceiveUpdate = false flag, respectively, all subsequent operations will not attempt to update the component.

If Fiber.alternate is available, the memoizedProps /* oldprops */ !== pendingProps /* new props */ happens and if they are not equal, the task is marked with the didReceiveUpdate = true flag.

Immediately after that, the Fiber.tag is checked. In our case, Fiber.tag === 3 /* IndeterminateComponent */. This tag indicates that we do not yet know what type of component is waiting for us, which means that it cannot be already mounted in any way.

There are three such tags that indicate an unmounted component

  • IndeterminateComponent
  • LazyComponent
  • IncompleteClassComponent

In these cases, the methods corresponding to each of the three types that mount the component will be launched.

All other types of tags are assigned after the mounting stage, which means that methods that only update the component will be launched for them.

In our example, for further work, it is required to determine the type of component that will be mounted, which happens approximately as follows

if (
  !disableModulePatternComponents &&
  typeof value === 'object' &&
  value !== null &&
  typeof value.render === 'function' &&
  value.$typeof === undefined
) {
  workInProgress.tag = ClassComponent;
  //...
} else {
  workInProgress.tag = FunctionComponent;
  //...
}

In other words, simplifying what was written above, a component is considered as a class, if it is represented as an object with the render method (in the form of a function). Otherwise, the component is considered functional.

In the case of class components, a cycle of mounting this class is started. The component lifecycle methods will be called here, and the internal this.state and this.props will be set.

Since class components are considered obsolete, we will not consider the process of mounting them in detail in this article, but will focus on the functional components.

In functional components, at this stage, the reconcileChildren function is called

/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L288

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  if (current === null) {
    // If this is a fresh new component that hasn't been rendered yet, we
    // won't update its child set by applying minimal side-effects. Instead,
    // we will add them all to the child before it gets rendered. That means
    // we can optimize this reconciliation pass by not tracking side-effects.
    workInProgress.child = mountChildFibers(
      workInProgress,      
      null,      
      nextChildren,      
      renderLanes,    
      );  
  } else {    
    // If the current child is the same as the work in progress, it means that    
    // we haven't yet started any work on these children. Therefore, we use    
    // the clone algorithm to create a copy of all the current children.    
    
    // If we had any progressed work already, that is invalid at this point so    
    // let's throw it out.    
    workInProgress.child = reconcileChildFibers(
      workInProgress,      
      current.child,      
      nextChildren,      
      renderLanes,    
    );  
  }
}

Despite the fact that we already know whether the component needs to be mounted or just updated, this function is universal and is used in many places. It checks the presence of current (aka Fiber.alternate) once again and decides which cycle the reconciler will follow.

Render phase

In this phase, the formation of the so-called "Virtual Tree" takes place. We already know the Fiber type (it can be a class component, a functional one, a hook call, or, for example, a context update). If Fiber is a component, its further branch of the lifecycle has also already been determined (whether it will be mounted or only updated).

Let's go back to our example. The reconciler has just worked out the reconcileChildren function, which in turn called mountChildFibers or reconcileChildFibers. However, in essence, both of these functions do the same operation - they create an instance of the ChildReconciler class, more precisely, there is no such class, but there is a corresponding type, and creating an instance is nothing more than calling a factory that returns an object of this type.

/packages/react-reconciler/src/ReactChildFiber.new.js#L1349

export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);

/packages/react-reconciler/src/ReactChildFiber.new.js#L265

function ChildReconciler(shouldTrackSideEffects) {
  ...
}

The only difference between reconcileChildFibers and mountChildFibers that the first one sets the shouldTrackSideEffects flag, which will later be used during ChildReconciler operation, mainly to determine whether Fiber itself and/or its children need to be deleted (an unmounted component does not need to be deleted, as well as its children).

Inside the class, the type of element that requires mounting/updating will be determined and the corresponding callbacks of the linked renderer (in our case, ReactDOM) will be called, and the renderer, in turn, will produce the corresponding mutations of the cloned Root element (i.e., the root of the virtual tree for the mounted/updated Fiber).

Here, the child elements of the first level will be bypassed (both a single component or an array can act as children). Each of the child elements will recursively undergo similar recoconsilation as its parent and, eventually, will fall into the same virtual tree as one of the branches of the tree.

Commit phase

In this phase, the final preparations of the virtual tree take place, including the execution of the necessary effects.

At each tick of processing a unit of "work", a check of the completion of this work is called. To do this, a special status flag is provided in the ReactFiberWorkLoop module

/packages/react-reconciler/src/ReactFiberWorkLoop.new.js#L299

let workInProgressRootExitStatus: RootExitStatus = RootInProgress;

Which can accept 7 statuses

/packages/react-reconciler/src/ReactFiberWorkLoop.new.js#L269

type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5 | 6;
const RootInProgress = 0;
const RootFatalErrored = 1;
const RootErrored = 2;
const RootSuspended = 3;
const RootSuspendedWithDelay = 4;
const RootCompleted = 5;
const RootDidNotComplete = 6;

By default, the flag is set to RootInProgress at the start of the reconciliation cycle.

Let's go back to the previous phase of render for a while. There, detecting another child Fiber, the reconciler puts the current child Fiber as the Root element and goes deeper recursively by its children. Having reached the deepest Fiber without children and processed it, the reconciler goes to the neighboring child, if there is one (there is a sibling link in Fiber for this). After processing all child Fibers at this level, the reconciler returns to the parent Fiber and processes neighboring elements already at this level. This will happen until the reconciler reaches the upper level, i.e. Fiber, which has no parent. After completing the top-level processing, the reconciler sets the workInProgressRootExitStatus = RootCompleted flag status, which serves as a signal to start the commit phase for this particular Root element.

Having received the RootCompleted status on the next tick, first of all, in the case of Suspenses, the reconciler calls the renderer's callback startSuspendingCommit, which allows the renderer to prepare an internal state for receiving suspensey resources. Basically, this is necessary to initiate the loading of styles of these resources.

Next, the reconciler traverses the entire virtual tree and binds the listeners to all such resources. Everything happens in one synchronous transaction.

Upon completion of the transaction, the reconciler, by calling waitForCommitToBeReady, requests the renderer whether it is ready to accept resources or needs to wait. The renderer, in turn, returns a subscriber function, which asynchronously tells the reconciler whether it is possible to commit the elements. At this stage, for example, React DOM waits for component styles to be loaded, only after that they can be linked to the real DOM.

If there are no more suspensey resources, the phase moves on to the next stages.

Flags and priorities are reset. The item cache is cleared.

Next, the execution of the so-called beforeMutationEffects takes place. This stage is divided into several sub-stages. First, the effects are traversed and prepared for all child elements. Then, depending on the type of Fiber, effects are performed on the current Fiber and further, effects of neighboring elements at this level.

After that, it's the turn of mutationEffects. They are executed on the current Fiber.

Next, the resetAfterCommit renderer callback is called. This means that all mutations of the tree have been made and now the renderer has the opportunity to do something at this point. For example, ReactDOM here restores the text selection, if there was one.

At the moment, the virtual tree is considered to be fully prepared, but it is not yet connected to the real DOM. Roughly speaking, this is the place to call, for example, componentWillUnmount

Layout phase

After the viral tree is fully formed and all Mutation effects are performed, it's time to start binding it to the real DOM (or output it in some other way).

At the moment, the reconciler has already reached the Root element. All that remains to be done is to mutate this Root element itself, after which, call the commitMount callback of the renderer, which will allow the renderer to make the last necessary changes. In the case of ReactDOM, set the focus if the element was one of input fields with the autoFocus parameter and set the src attribute if the element was an img tag.

Next, it's time to let the browser render the changes. To do this, the browser should be asked to step back to the end of the frame by navigator.scheduling.isInputPending.

Additionally, the counter for the number of re-renderers of the Root element is incremented in order to detect an infinite updates loop.

The remaining flags and timers are being reset.

At this point, the process of reconciliation can be considered complete. As soon as new changes occur in any of the Fibers, a new reconciliation cycle will be started for this Fiber, starting from the element that was changed (or added).


My telegram channels:

EN - https://t.me/frontend_almanac
RU - https://t.me/frontend_almanac_ru

Русская версия: https://blog.frontend-almanac.ru/z1S4MzuquZd