This article will attempt to formalize my understanding of how the React library work internally to allow us to create complex web-applications. Without any further ado, let's dive right into it.
HTML and the DOM
Let's start with two terms that you will encounter a lot in web development: HTML and DOM. HTML stands for Hypertext Markup Language. A web-page is simply a document written in HTML.
DOM stands for Document Object Model. The DOM is the representation of the web-page that is understood by the web-browser. The DOM is represented as a tree of HTML tags. (In a Tree data structure, each node can have zero or more child-nodes. The child-nodes belonging to the same parent are known as siblings and nodes without children are known as leaf nodes.)
The DOM provides APIs through which we can make changes to the web-page. For example using methods like document.getElementById
etc.
Components
React is a Component-based library. Components are a layer of abstraction in that they allow us to describe the UI we wish to generate while leaving the implementation details to React.
React Components can be created in two ways:
1) Using Classes
2) Using Functions
While using Class-based components we use the render()
method to define the JSX that should be displayed on the browser whereas while using Functional components we directly return
the JSX that should be rendered as the result of the function.
JSX and React.createElement
JSX allows you to write HTML code directly inside JavaScript functions. It is syntactic sugar for the method React.createElement()
.
JSX is converted to regular JavaScript via a transpiler known as Babel (using the preset babel/preset-react).
Let's examine this in detail using an example:
This is a simple Component that I already had from a hobby project:
export default function PlaylistDetail(props){
return (
<div>
<p>Playlist Name</p>
<p>Created by</p>
<div>Track List</div>
</div>
);
}
Compiling this code using Babel produces the following output:
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = PlaylistDetail;
function PlaylistDetail(props) {
return /*#__PURE__*/ React.createElement(
"div",
null,
/*#__PURE__*/ React.createElement("p", null, "Playlist Name"),
/*#__PURE__*/ React.createElement("p", null, "Created by"),
/*#__PURE__*/ React.createElement("div", null, "Track List")
);
}
From this, we conclude that JSX tags are ultimately converted to calls to the React.createElement()
method. Now you may be thinking: That's great! But why should I care? We'll get to that in a bit. Hang tight, all of this will make a lot more sense in a bit, I promise. We just need to understand what an Element is in React.
Elements and Component Instances
An Element in React is a simple JavaScript object that describes the instance of a component. Think of Components as being general and Component instances as specific (i.e. A Component with specific props and state). Whenever the props and state of a Component change it results in a different instance of the Component.
The Element object representing the instance of the PlaylistDetail Component seen previously is shown below:
{
$$typeof: Symbol(react.element),
key: null,
props: {
children: [
{
$$typeof: Symbol(react.element),
key: null,
props: {
children: "Playlist Name"
},
ref: null,
type: "p"
},
{
$$typeof: Symbol(react.element),
key: null,
props: {
children: "Created by"
},
ref: null,
type: "p"
},
{
$$typeof: Symbol(react.element),
key: null,
props: {
children: "Track List"
},
ref: null,
type: "div"
}
]
},
ref: null,
type: "div"
}
The $$typeof
field is used to prevent Cross site scripting Attacks (XSS) since it should always contain a Symbol
which prevents the JavaScript object from being parsed from plain JSON objects.
The key
field is used to optimize performance by allowing elements generated from a list to be uniquely identified.
The props
field is an object containing all the props that are passed to the Component. The props
field also stores children
which is an array containing the children of the current element.
The type
field refers to the type of the object either a native HTML tag or a Component type. In this example the type is a native HTML div.
The ref
field stores a reference to the actual DOM node. (This can be utilized via the useRef
hook).
Thus, we see that React components (and their child components) can be represented as objects (with nested objects) similar to the way the web-page is itself represented via the tree structure of the DOM. Hence this representation of React components as a tree of objects is known as the Virtual DOM.
Why bother?
The reason React goes through this cumbersome process of maintaining object representations of Components is simple: Performance. It's computationally much cheaper to create and manipulate JavaScript objects than it is to manipulate the DOM.
This allows React to consolidate all the changes that need to be made in a render cycle and perform one bulk update to the DOM instead of performing many smaller changes.
Reconciliation
Now, let's take a look at how React determines when a Component needs to be re-rendered and what exactly has changed since the previous render cycle. This information is required in order to prevent unnecessary DOM updates. (i.e. updating DOM nodes that have not been changed since the previous render)
The process of maintaining the tree of Elements whenever a Component's props or state change is known as Reconciliation. React does this by maintaining the state of the Element tree from the previous render and comparing it to the new Element tree using an diffing algorithm.
The problem of comparing the previous tree (or graph) with the new tree is known in Computer Science as the Graph Edit Distance Problem.
Theoretically, this problem falls under a class of problems known as NP-hard (non-deterministic polynomial-time) but there are approximation solutions to this problem in O(n^3). React's diffing algorithm uses certain optimizations that further reduce the runtime to O(n). (n is number of nodes in the tree).
Optimizations, you say?
The optimizations React uses to achieve this performance boost are:
- If an element's
type
field has changed, then React re-creates the entire subtree (for this particular element and it's children). - React uses the
key
field to uniquely identify elements generated dynamically from a list in order to prevent recreating the entire list when list operations are performed (E.g. when elements added or deleted, or list is reversed etc.). React simply makes the relevant changes directly in the DOM. - When attributes of native DOM elements are changed but the element type remains the same then only the attributes that have changed are updated (instead of re-creating the entire DOM Node).
Rendering vs Reconciliation
The process of Reconciliation (that we looked at in the previous section) determines what changes need to be made to the DOM.
The process of actually making these changes is known as Rendering. There is a separation of concerns between the packages responsible for Reconciliation and Rendering.
The react
package is only concerned wit Reconciliation.
The react-dom
package is concerned with actually performing the rendering on the browser. It does this via the ReactDOM.render()
method.
This separation of concerns allows react-native
to replace react-dom
while rendering to mobile apps.
TO BE CONTINUED
Top comments (0)