Skip to content

Instantly share code, notes, and snippets.

@CarlMungazi
Last active August 21, 2019 08:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CarlMungazi/47ca766d7ab09d79b3f96e1dd7b38e29 to your computer and use it in GitHub Desktop.
Save CarlMungazi/47ca766d7ab09d79b3f96e1dd7b38e29 to your computer and use it in GitHub Desktop.
What happens when we invoke ReactDOM.render()?

What happens when you invoke ReactDOM.render()?

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('root'),
  () => {}
);

ReactDOM is an object which exposes a number of top-level APIs. According to the docs, it provides 'DOM-specific methods that can be used at the top level of your app and as an escape hatch to get outside of the React model if you need to'. One of those methods is render().

Some background notes

Before going further, it is worth having a general understanding of React's internal workings. In React 15, updates were done in a synchronous manner. This created the risk of updates running over the 16ms (100ms / 60fps) needed for computation and janky UIs. React 16 introduced fiber, a re-write of React's reconciliation algorithm which allowed updates to be scheduled asynchronously and prioritised. This was done by re-implementing the browser's built-in call stack. Doing so made it possible to interrupt the call stack and manually manipulate the stack frame in a way specially tailored for React components. At the core of this re-write is a data structure called a fiber, which is an object that is mutable and holds component state and DOM. It can also be thought of as a virtual stack frame. Fiber's architecture is split into two phases: reconciliation/render and commit. Over the course of this article we will touch upon some of its aspects but more extensive explanations can be found here:

The journey begins

ReactDOM.render() actually wraps another function and invokes it with two additional arguments, so its declaration is simple. Below is the wrapped function:

function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
  ...
  let root = container._reactRootContainer;
  if (!root) {
    // Initial mount
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function () {
        const instance = DOMRenderer.getPublicRootInstance(root._internalRoot);
        originalCallback.call(instance);
      };
    }
    // Initial mount should not be batched.
    DOMRenderer.unbatchedUpdates(
      () => {
        if (parentComponent != null) {
          root.legacy_renderSubtreeIntoContainer(
            parentComponent,
            children,
            callback,
          );
        } else {
          root.render(children, callback);
        }
      }
    );
  } else {
    ...
  }
  return DOMRenderer.getPublicRootInstance(root._internalRoot);
}

The first thing React does is create the fiber tree for our application. Without this, it cannot handle any user updates or events. The tree is created by calling legacyCreateRootFromDOMContainer, which returns the following object:

{
  containerInfo: div#root
  context: null
  current: FiberNode {tag: 3, key: null,}
  didError: false
  earliestPendingTime: 0
  earliestSuspendedTime: 0
  expirationTime: 0
  finishedWork: null
  firstBatch: null
  hydrate: false
  interactionThreadID: 1
  latestPendingTime: 0
  latestPingedTime: 0
  latestSuspendedTime: 0
  memoizedInteractions: Set(0) {}
  nextExpirationTimeToWorkOn: 0
  nextScheduledRoot: null
  pendingChildren: null
  pendingCommitExpirationTime: 0
  pendingContext: null
  pendingInteractionMap: Map(0) {}
  timeoutHandle: -1
}

This is called a fiber root object. Every React app has one or more DOM elements that act as containers and for each of these containers, this object is created. It is on this object we find a reference our fiber tree via the current property, and its value is:

{
  actualDuration: 0
  actualStartTime: -1
  alternate: null
  child: null
  childExpirationTime: 0
  effectTag: 0
  elementType: null
  expirationTime: 0
  firstContextDependency: null
  firstEffect: null
  index: 0
  key: null
  lastEffect: null
  memoizedProps: null
  memoizedState: null
  mode: 4
  nextEffect: null
  pendingProps: null
  ref: null
  return: null
  selfBaseDuration: 0
  sibling: null
  stateNode: {current: FiberNode, containerInfo: div#root,}
  tag: 3
  treeBaseDuration: 0
  type: null
  updateQueue: null
}

This is a fiber node and it is found at the root of every React fiber tree (this is the function which creates this object). This node is actually a special type of fiber node called a HostRoot node and it acts as a parent for the uppermost component in our application. We know it is a HostRoot node because the value of its tag property is 3 (you can find the full list of fiber node types here). Notice how at this stage, a lot of the properties, especially child , are null. We'll come back to this later.

After creating the tree, React checks if we provided a callback as the third argument of the render call. If so, it grabs a reference to the root component instance of our application (in our case, it's the <h1> element) and then makes sure our callback will be called on this instance later on.

Into the thick of it

All the stuff that has happened prior to this point is preparation for the work that will render our application on screen. So far, we have a tree but it does not resemble our UI. This problem is solved with the following code:

// Initial mount should not be batched.
unbatchedUpdates(function () {
  if (parentComponent != null) {
    root.legacy_renderSubtreeIntoContainer(parentComponent, children, callback);
  } else {
    root.render(children, callback);
  }
});

Root is an object with only one property (the property _internalRoot which holds a reference to the root fiber object). It was created by calling new on the ReactRoot function, so if you look up its internal [[Prototype]] chain you will find the following method:

ReactRoot.prototype.render = function (children, callback) {
  var root = this._internalRoot;
  var work = new ReactWork();
  callback = callback === undefined ? null : callback;
  {
    warnOnInvalidCallback(callback, 'render');
  }
  if (callback !== null) {
    work.then(callback);
  }
  updateContainer(children, root, null, work._onCommit);
  return work;
}

Remember our HostRoot fiber node earlier with all the null values? In the function above, we can access it via root.current. After updateContainer() has finished its work, this is what it looks like:

{
  actualDuration: 6.800000002840534
  actualStartTime: 95592.79999999853
  alternate: FiberNode {tag: 3, key: null,}
  child: FiberNode {tag: 5, key: null,}
  childExpirationTime: 0
  effectTag: 32
  elementType: null
  expirationTime: 0
  firstContextDependency: null
  firstEffect: FiberNode {tag: 5, key: null,}
  index: 0
  key: null
  lastEffect: FiberNode {tag: 5, key: null,}
  memoizedProps: null
  memoizedState: {element: {}}
  mode: 4
  nextEffect: null
  pendingProps: null
  ref: null
  return: null
  selfBaseDuration: 2.5999999925261363
  sibling: null
  stateNode: {current: FiberNode, containerInfo: div#root,}
  tag: 3
  treeBaseDuration: 3.1999999919207767
  type: null
  updateQueue: {baseState: {}, firstUpdate: null,}
}

And the value in the child property is now a fiber node for the h1 element:

{
  actualDuration: 4.100000005564652
  actualStartTime: 95595.49999999581
  alternate: null
  child: null
  childExpirationTime: 0
  effectTag: 0
  elementType: "h1"
  expirationTime: 0
  firstContextDependency: null
  firstEffect: null
  index: 0
  key: null
  lastEffect: null
  memoizedProps: {children: "Hello, world!"}
  memoizedState: null
  mode: 4
  nextEffect: FiberNode {tag: 3, key: null,}
  pendingProps: {children: "Hello, world!"}
  ref: null
  return: FiberNode {tag: 3, key: null,}
  selfBaseDuration: 0.5999999993946403
  sibling: null
  stateNode: h1
  tag: 5
  treeBaseDuration: 0.5999999993946403
  type: "h1"
  updateQueue: null
}

As you can see, the fiber tree now reflects the UI we want rendered. How did that happen? Below is a summary of the steps which took place in order to get us to this stage:

Schedule the updates

React has an internal scheduler which it uses for handling co-operative scheduling in the browser environment. This package includes a polyfill over window.requestIdleCallback, which is at the heart of how React reconciles UI updates. During the process each fiber node is given an expiration time. This is as a value which represents a time in the future the work (the name given to all the activities which happen during reconciliation) being done should be completed. 

This calculation involves checking if there's any pending work with a higher priority as well as the nature of the work currently going on. In our case, it is a synchronous update but in other scenarios it could be interactive or asynchronous. Examples of low priority updates include network responses or clicking a button to change colours. High priority updates include user input and animations.

Create an update queue

In our example, we only have one fiber tree and since it has no update queue, one has to be created and assigned to the updateQueue property. This queue is created with the createUpdateQueue function and it takes the fiber's node's memoizedState as its argument. Memoized state refers to the state used to create the node's output. The queue is a linked list of prioritised updates and like fiber trees, it also comes in pairs. The current queue represents the visible state of the UI.

Create the work-in-progress tree and do work on it

When the work is actually being performed, React checks its internal stack to determine whether it's starting with a fresh stack or resuming previously yielded work. Soon after this check it uses a technique known as double buffering to create the workInProgress tree. React works with two fiber trees - one named current which holds the current state of the UI and another named workInProgress which reflects the future state. Every node in the current tree has a corresponding node in the workInProgress tree created from data during render. 

Once the workInProgress is rendered on the screen, it becomes the new current tree. The workInProgress tree also has an update queue but it can be mutated and processed asynchronously before being rendered.

DOM element creation

The fiber node for the h1 element is created during the work on the workInProgress tree. A few calls later, this function uses the DOM API to create our h1 element, which so far has been a React element object. When the application has rendered, you can access the element by typing document.querySelector('h1'). If you check its properties, you will find one beginning with __reactInternalInstance$. This property holds a reference to the element's fiber node. React also assigns the element's text content and then later appends it to our root DOM element. By the time its appended, React has already entered its commit phase.

And that's all there is to it

In our example we have rendered the most basic of UIs but React has done a lot of work to prepare and display it. Like most frameworks created for building complex applications, it does this work to prepare for all the eventualities associated with such a task. And with the example application being static, we have not touched on any of the code involved in state updates, lifecycle hooks and the rest of it.

As this is my first time doing a deep dive on React, I am sure I have missed things out or incorrectly explained others, so please, leave your feedback below!

Notes on article

  • This created the risk of updates running over the 16ms (100ms / 60fps) needed for computation and janky UIs (explain the 16ms bit much clearer)

  • Call updateContainer
    • log the the current (start?) time of this process via requestCurrentTime()
      • requestCurrentTime() involves checking if there's pending work which is high priority
    • log the expiration time for our HostRoot node's update via computeExpirationForFiber()
      • computeExpirationForFiber() involves checking the nature of any work currently going on such as interactive updates or async updates. In our case, it is a sync update
    • call updateContainerAtExpirationTime()
      • assign the context property on our root fiber object
      • call scheduleRootUpdate()
        • call createUpdate()
          • return an object with the expiration time, tag, payload, callback, next, and nextEffect properties. Assign this to the property update
        • update.property is assigned the react element object for our h1 element. update.callback is assigned the callback
        • call flushPassiveEffects()
          • nothing happens but this looks to be a function which deals with scheduling
        • call enqueueUpdate() with our hostroot and update objects
          • assign hostroot.alternate. this always points to the corresponding fiber tree. Can we assume we are dealing with the WIP tree at this stage?
          • because we only have one fiber and it has no update queue, call createUpdateQueue() (check notes at top of function) with fiber.memoizedState
            • return an object with the following properties: baseState, firstUpdate, lastUpdate, firstCapturedUpdate, lastCapturedUpdate, firstEffect, lastEffect, firstCapturedEffect, lastCapturedEffect. assign it to queue
          • we only have one queue, so we call appendUpdateToQueue() with queue and update
            • our queue is empty so queue.firstUpdate and queue.lastUpdate are assigned update
        • call scheduleWork() with our hostroot and expiration time
          • call scheduleWorkToRoot() with our hostroot and expiration time
            • call recordScheduleUpdate().
              • it checks enableUserTimingAPI which is a feature flag set to __DEV__ but because we have not set this, nothing happens
            • if we were dealing with a class component, react would create a warning for any invalid updates
            • update our hostroot's expiration time if it is lower than the current time (which it is). Do the same for hostroot.alternate if it is assigned AND has an expiration time lower than the current expiration time
            • if hostroot wasn't hostroot and had a parent, we would update its expiration time via its parent. But because it's hostroot, we assign the variable root to the fiber root object
            • tracing which interactions trigger each commit. in our case, we return root because we have no interactions. fun fact: __interactionsRef.current.size which holds this information, is actually React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.SchedulerTracing.__interactionsRef. See this
            • return root
          • the return value from scheduleWorkToRoot() is assigned to root, which is the root fiber object
          • call markPendingPriorityLevel()
            • return a call to findNextExpirationTimeToWorkOn()
              • set the expirationTime on our root fiber object
          • check if we need to schedule the root fiber object for an update
            • we do need to schedule, so call requestWork(), which is called by the scheduler whenever a root receives an update
              • call addRootToSchedule()
                • set the nextScheduledRoot on root to the current root, since it's not already scheduled
              • check if the expirationTime value matches the Sync variable, which is the max integer size in V8 for 32-bit systems. In our case, it is, so call performSyncWork()
                • call performWork() with Sync and false. This function keeps working on roots until there is no more work or a higher priority event comes along
                  • call findHighestPriorityRoot()
                    • enter into a loop which performs work as long as there is a root object to work on
                  • call performWorkOnRoot()
                    • check if we're dealing with async or sync/expired work. in our case, it's sync/expired work
                    • call renderRoot(), where it appears we do A LOT
                    • call flushPassiveEffects()
                    • return nothing
                    • we're not using hooks, so we do have a dispatcher without hooks
                    • a check on whether we're in a fresh stack or resuming previous work. we're in a fresh stack so call resetStack(). we start working from the root
                    • call ReactStrictModeWarnings.discardPendingWarnings()
                    • call checkThatStackIsEmpty() and the stack is empty
                    • call createWorkInProgress() and assign the return value to nextUnitOfWork. This creates an alternate fiber to do work on. it uses a double buffering pooling technique
                    • call createFiber() to create our alternate fiber node
                    • the current set of interactions are assigned to memoizedInteractions() so they can be re-used in hot functions such as renderRoot()
                    • call startWorkLoopTimer() on our alternate fiber
                    • call beginMark()
                    • call performance.mark() with "(React Tree Reconciliation)"
                    • call formatMarkName()
                    • call resumeTimers()
                    • call resumeTimersRecursively()
                    • call workLoop()
                    • flush the work without yielding by calling performUnitOfWork() on our alternate fiber
                    • call startWorkTimer() to see if starting this work creates more work
                    • call shouldIgnoreFiber()
                    • in our case, this returns true
                    • call setCurrentFiber()
                    • call assignFiberPropertiesInDEV(), which is a dev thing
                    • call startProfilerTimer()
                    • call unstable_now()
                    • call Performance.now()
                    • assign the start time for our fiber to actualStartTime
                    • call beginWork()
                    • call hasContextChanged()
                    • call updateHostRoot()
                    • call pushHostRootContext with our WIP fiber
                    • call pushTopLevelContextObject()
                    • call push() twice, which appears to be dealing with react's stack implementation
                    • call pushHostContainer()
                    • push() is called 4x and pop() once
                    • call pushHostContainer()
                    • call getRootHostContext()
                    • call getChildNamespace()
                    • call getIntrinsicNamespace(), which returns "http://www.w3.org/1999/xhtml"
                    • call updatedAncestorInfo()
                    • call processUpdateQueue()
                    • call ensureWorkInProgressQueueIsAClone()
                    • call cloneUpdateQueue so we can give WIP its own queue as it is the one currently being processed
                    • enter a while loop in which we work on our h1 element
                    • call getStateFromUpdate() with the WIP and h1 as part of the arguments
                    • call _assign(), which is from https://www.npmjs.com/package/object-assign, to merge the partial state and previous state of our h1 element. need to find out where this is declared
                    • the element has been updated and its base and result states were found to be the same
                    • recalculate the remaining expiration time
                    • call reconcileChildren() because we preparing to reset the hydration state
                    • call reconcileChildFibers()
                    • call placeSingleChild(), wraps a call to
                    • call reconcileSingleElement()
                    • call createFiberFromElement()
                    • call createFiberFromTypeAndProps()
                    • call createFiber()
                    • call new FiberNode(tag, pendingProps, key, mode) to create a fiber node for our h1 element
                    • call coerceRef()
                    • our h1 fiber node is now a child on WIP
                    • call resetHydrationState()
                    • call stopProfilerTimerIfRunningAndRecordDelta(), which seems to be a DEV thing
                    • call resetCurrentFiber()
                    • the loop goes around again, this time doing slightly different work than the previous iteration
                    • call completeUnitOfWork(), which immediately jumps into a loop
                    • call setCurrentFiber()
                    • call completeWork()
                    • call popHostContext()
                    • make two pop() calls, which I imagine is clearing the call stack
                    • call getHostRootContainer()
                    • call requiredContext()
                    • call getHostContext()
                    • call requiredContext()
                    • call popHydrationState() WIP was not hydrated, so call createInstance()
                    • call validateDOMNesting(), which I imagine makes sure our elements are correctly nested
                    • call isTagValidWithParent()
                    • call findInvalidAncestorForTag()
                    • call updatedAncestorInfo()
                    • call createElement(), the function that creates DOM elements!
                    • get the owner document by calling getOwnerDocumentFromRootContainer()
                    • the parent namespace is "http://www.w3.org/1999/xhtml"
                    • call getIntrinsicNamespace() which confirms the namespace
                    • call isCustomComponent()
                    • call createElement on the owner document (document.createElement) and assign it domElement
                    • our h1 element is created
                    • call precacheFiberNode() with the h1 element and its fiber node
                    • this is where that __reactInternalInstance$ property is created
                    • call updateFiberProps()
                    • call appendAllChildren() with the h1 element and WIP among the arguments but nothing appears to happen?
                    • call finalizeInitialChildren() condition part of an if clause
                    • call setInitialProperties()
                    • do DEV related stuff
                    • call assertValidProps()
                    • call setInitialDOMProperties()
                    • call setTextContent()
                    • assign hello, world to h1.textContent
                    • return call to shouldAutoFocusHostComponent(), which is false
                    • call stopWorkTimer()
                    • call resetChildExpirationTime()
                    • call resetCurrentFiber()
                    • break the work loop because we have finished
                    • call resetContextDependences()
                    • call resetHooks()
                    • call stopWorkLoopTimer() because the entire tree is done
                    • call pauseTimers()
                    • call endMark()
                    • call onComplete() because we're ready to enter the commit phase
                    • assign pendingCommitExpirationTime and finishedWork on root
                    • call completeRoot() to commit our root
                    • call commitRoot()
                    • call startCommitTimer()
                    • call beginMark()
                    • call markCommittedPriorityLevels()
                    • call findNextExpirationTimeToWorkOn()
                    • call prepareForCommit()
                    • call isEnabled()
                    • call getSelectionInformation()
                    • call getActiveElementDeep()
                    • call getActiveElement()
                    • call hasSelectionCapabilities()
                    • call setEnabled()
                    • call startCommitSnapshotEffectsTimer()
                    • call beginMark()
                    • call invokeGuardedCallback()
                    • lost the trace here but appendChildToContainer() to append our h1 element to the root DOM element
                    • call stopCommitHostEffectsTimer()
                    • call resetAfterCommit()
                    • current now becomes WIP made calls but did not log them
                    • call onCommitRoot()
                    • call onCommitFiberRoot() - need to investigate this call because something happened
                    • call onCommit()
                    • call findHighestPriorityRoot()
                    • call finishRendering()
  • return all the way to ReactRoot.prototype.render()

Resources/Questions for future articles

How does React implement its own call stack?

This appears to be part of React's call stack implementation?? There is an array named fiberStack. What is this?

Also, see this: https://www.facebook.com/notes/andy-street/react-native-scheduling/10153916310914590 and https://giamir.com/what-is-react-fiberhttps://giamir.com/what-is-react-fiber

How does React batch updates?

How does React use double buffering?

The workInProgress fiber is created using a double buffering pooling technique. It is created lazily Update queues are created lazily

TL;DR for React Fiber

  • It exists to solve the problem of keeping the UI in sync with the state of the application. This process is called reconciliation

  • Previously, updates were synchronous and recursive (which risked them running over the 16ms (100ms / 60fps) needed for computation and dropping frames) but are now scheduled asynchronously and prioritised.

  • window.requestIdleCallback() was introduced to deal with the problem. It allows your code to co-operate with the event loop by telling you how much time you can safely use without causing any laggy behaviour. React implemented their own version because they found requestIdleCallback() too restrictive

  • Fiber re-implements the built-in call stack to allow call stack interruptions and manual stack frame manipulations, specially done for React components. A fiber can be thought of as a virtual stack frame

  • All the activities which happen during reconciliation are known as work and the type of work is determined by the type of React element

  • Every React app has one or more DOM elements that act as containers. React creates a fiber root object for each of those containers which can be accessed via the _reactRootContainer._internalRoot property on the DOM element

  • This container has a reference to the fiber tree and it can be accessed via _reactRootContainer._internalRoot.current

  • The fiber tree starts with the HostRoot fiber node, which is a special type of fiber node that acts as a parent for the topmost component. This can be accessed via _reactRootContainer._internalRoot.current.tag. If the tag value is 3, then you are dealing with a HostRoot.

  • The HostRoot fiber node has a reference back to the fiber root object via the _reactRootContainer._internalRoot.current.stateNode property. You can also access the fiber tree from a component's instance via the __reactInternalFiber property.

  • On render, data from every React element is merged into the fiber tree. Each element has a corresponding fiber node and each node is a mutable data structure which holds component state and DOM.

  • Each fiber represents a unit of work which needs to be done and this work can be tracked, scheduled, paused and aborted. So during each update this node is updated.

  • React works with two fiber trees - one named current which holds the current state of the UI and another named workInProgress which reflects the future state. Every node in the current tree has a corresponding node in the workInProgress tree created from data during render. Once the workInProgress is rendered on the screen, it becomes the new current tree. Each tree has a reference to its counterpart via the alternate property.

Writing your own virtual DOM lib

https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060 - writing a basic virtual DOM is not that hard

https://jsreport.io/the-ultimate-guide-to-javascript-frameworks/ - just look at the options available - and how many of them implement the virtual dom paradigm

https://github.com/jorgebucaran/superfine - a vdom library that explains some of the concepts in its readme

https://evilmartians.com/chronicles/optimizing-react-virtual-dom-explained - a real world explainer of vdom's benefits

How is JSX turned into a React.createElement call

https://jaketrent.com/post/how-jsx-transform-works/

https://reactjs.org/docs/jsx-in-depth.html

https://babeljs.io/docs/en/next/babel-plugin-transform-react-jsx.html

https://dev.to/sarah_chima/9-things-you-should-know-about-jsx-3bm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment