Inner workings of React

AkashSDas
8 min readJan 14, 2024

--

React allows you to focus on describing the UI you want, rather than focusing on the details of how its being inserted in the DOM. This declarative nature of React is what makes it maintainable and the way it achieves this is what makes it performant.

Inner workings of React

We never have to worry about how a header or a footer is being inserted in to the page. They’re implementation details. We only describe them and React takes care of its insertion in the DOM.

DOM manipulation
DOM manipulation

A web page is a document just like PDF and Word. We can edit this document using api called DOM.

Manipulating DOM is the most expensive operation that we can do. Not only that, browser also has to repaint, load network requests, frame animation, etc and all of this has be done on a single thread as JavaScript is single threaded. Everything happens from start to finish.

Its much easier to generate all the changes we want to make and then update rather than up on every change. This is where React and React DOM comes into play.

Elements and Components

JSX, JavaScript XML is the primary way of writing UI in a function/class components. When a component is rendered, if its a function component then React calls it directly with the assigned props, and if its a class component then React creates a new instance of the class and calls the render method on it.

In this process, props are being consumed, states are being deduced, and the return value is an element. An element is a plain JavaScript object that describes a component (component element) or a HTML tag (DOM element).

Function component returning DOM element
Function component returning DOM element

Let’s go through an element:

  • type - it can be a string referencing a HTML tag (DOM element) or it can be a reference to a component (component element).
  • key - its used to uniquely identify an element within a bunch of elements.
  • ref - its a reference to the actual DOM node.
  • props.children - it can be null, an element, or a list of elements.
  • $$typeof - it has a value of Symbol. Since JSON can’t have symbols, React is protected from XSS attach. Every single element that gets created has it. React expects every element to have this property and expects it to have that value of react.element passed inside it. If React doesn’t finds the same value of $$typeof then it rejects it.
var s1 = Symbol("symbol 1");
console.log(s1);

var s2 = Symbol("symbol 2");
console.log(s2);

console.log(s1 === s1); // true
console.log(s1.toString()); // Symbol(symbol 1)

The element object is returned when we call the function component or invoke the render method on the class component’s instance. We never explicitly call the function though, instead we write something like <Component />.

Actual output of component
Actual output of component

React builds a whole tree of this element object from every component. The resultant object is called Virtual DOM. Creating this is cheaper as opposed to writing to the DOM for every change.

Its cheaper to create JavaScript objects that writing to the DOM.

Reconciliation

React creates a tree of elements every time the render function is called. So to tell the difference between the previous VDOM and the new VDOM React uses the diffing algorithm. This is done to only update the parts of actual DOM which has actually changed.

In the initial render React has to fully insert the VDOM in the real DOM. Its expensive but there’s no way around. Later on, React uses the diffing algorithms to figure out which part of the DOM should be updated.

Normally for the diffing algorithm if we’ve n elements then we’ll have n^3 operations. React manages n elements to have smaller than n operations to get the diff.

This optimization is possible because of 2 assumptions:

  • Two different types of elements will produce 2 different types of tree. This is why all of the children get re-rendered when parent re-renders.
  • When we’ve list of child elements which often changes, we should provide an unique key as a prop so that React doesn’t have to work on items which have not changed.

Type change

Whenever the type field for the root element changes, React tears down that tree and builds a new one. React assumes that if the parent has changed its children would also have changes or the dependencies they relied on have changed (eg: API calls and such other side effects).

This is also the first point on how React speeds up the diffing algorithm.

Type change
Type change

While tearing down, all of the DOM nodes are destroyed, component instance will receive componentWillUnmount and equivalent useEffect will fire. So when we start building a new tree, all of the new components are there, and then we’re going to start rendering again.

The new DOM nodes are inserted in the DOM and then components will receive componentWillMount and then componentDitMount, and for function components, their hooks run as appropriately. Any state associated with the old tree will be lost.

DOM and component elements

There’re some differences in reconciliation for the native DOM element and component element. For example, in case of DOM element if the className has changed then when the element is rendered, React finds the DOM node and just modifies it. No need to change anything else because its all the same. After this, React will recurse on its children.

DOM and component elements
DOM and component elements

When a component element updates, its state and props gets updated to match the new element that will be created. After that the render function is called, the diffing algorithm recurses on the previous and the new trees until the end of the tree where there’s nothing left to change.

For class component, the component instance remains the same across the renders.

Children

The key attribute on elements is very useful for differentiating between the children. Consider the case where we don’t have key. A list of items in which we add a new item in the end.

Wrong children JSX
Wrong children JSX

React will go through the list items:

  • the first is there,
  • the second is there,
  • but there isn’t the third, so React will add the third item

Now, consider the insertion of new item in the start rather than the end.

Wrong children JSX
Wrong children JSX

React will go through the list items:

  • the first item has changed, so destroy old DOM node and create a new one, third.
  • then same for the second item, and at last when it see’s that second is the new item it adds it in the end.

If the list is big then this operation will cost a lost. So instead we can add key and tell React which elements are same and reduce the number of updates to the DOM. The goal of React is to touch DOM as little as possible.

Correct children JSX
Correct children JSX

The key shouldn’t be index of an array because, let’s say we’ve inverted our list and now our index will remain the same for the key and hence React won’t be updating anything.

Using array index for jsx key
Using array index for JSX key

Always use unique identifiers for key no matter what. Use lodash util (uuid is a bit expensive). key should be a string. React does strict comparison.

The diffing algorithm is being able to do shifting of elements among children. This doesn’t happens when we shift children like making a child its child’s element or swap parent with child. This will be considered as new element and React will re-render.

Rendering

How things end up the in the actual DOM? This is where rendering comes in to the game.

React on its own is just a library that create components, elements, and handles the diffing. Its the renderers like React DOM, React Native, etc… that takes care of the implementation. They start the reconciliation. They generate the tree of elements and insert it wherever it has to be inserted. We can create our own renderer using the react-test-render package.

import ReactDOM from "react-dom";
import App from "./App";

// the `render` method starts the reconciliation process
ReactDOM.render(<App />, document.getElementById("root"));

React DOM

import { AppRegistry } from "react-native";
import App from "./App";

AppRegistry.registerComponent("MyApp", () => App);

React Native

Since we use the render method from the renderer package only once, then how come the renderer handles so many things? This is because React communicates with the renderer internally. There’re many ways, one of such method is the state setters:

// Inside ReactDOM
var instance = new Component();
instance.props = props;
instance.updater = ReactDOMUpdater

// Class components
setState(partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback);
}

// Functional components
var React = {
// ...

__currentDispatcher: null,

useState(initialState) {
return React.__currentDispatcher.useState(initialState);
},

// ...
}

// Inside ReactDOM
var previousDispatcher = React.__currentDispatcher;
React.__currentDispatcher = ReactDOM.__dispatcher;
var result;
try {
result = Component(props)
} finally {
React.__currentDispatcher = previousDispatcher;
}

React Fiber

The actual rendering process is done via React Fiber. It gives us non-blocking renders. It allows us to pause work, restart work, or just straight up shut it down. An example of this is that in useEffect we have a series of API calls, Fiber will allow the whole componnent to stop right in the middle of that process and will throw away the progress made.

The question remains how does Fiber halts some execution in between as JavaScript is single threaded and will run from start to the end.

Under the hood we’ve an element and the DOM node, and Fiber sits between them and does those insertions for us.

Suspense, error boudaries, dynamic imports, etc are all powered by Fiber.

--

--