Let’s build a React from scratch: Part 1 — VirtualDOM and Renderer

Arindam Paul
12 min readMar 28, 2022

--

Disclaimer: The content below is just for learning and have some key insights while using framework like React. While the inspiration is React, the idea is to deliver the core concepts of what it takes to build a library like React at its most basic level.

As a child, I always enjoyed learning things by building them from scratch. It's fun and Satisfying to the core. Well, today I am writing this series to bring out that childhood me a little bit. I thought what would be a better topic than learning React (without React), as it's so popular and reachable to many many readers.

Special thanks and shout out to my inspiration, for this series from Tejas Kumar, and his amazing talk on Deconstructing React

The content will be covered in multi-part series, especially 4 for this one. I wanted to cover all the main aspects which make React what it is.

😇 Setting Things up

In order for us to get to the essence of this series, I would love it if you can ride along with me. I mean literally! (virtually of course :)). So first things first, how do we set up our stage for what’s to come.

We will keep it simple. I will be using Typescript to help me out with some JSX Syntax and we will run it using Stackblitz (web-container). So all you need is a modern browser (Chromium-based) that’s all. Or You can choose your favorite setup as long as you can run Typescript that’s all we will need.

This is a blank node-js template starter screen you will see when you start off in stackblitz. First, we install typescript and initialize it for the project.

npm i -D typescript
warn preInstall No description field
warn preInstall No repository field
warn preInstall No license field
┌ [1/3] 🔍 Resolving dependencies
└ Completed in 4.561s
┌ [2/3] 🚚 Fetching dependencies
└ Completed in 1.11s
┌ [3/3] 🔗 Linking dependencies
└ Completed in 0.301s
success Saved lockfile "package-lock.json"
success Updated "package.json"
success Install finished in 5.986s~/projects/node-gulnyl 6snpx typescript --initCreated a new tsconfig.json with:

We will need Typescript primarily for the JSX syntax. So let’s go ahead and enable the same in tsconfig.json .

So we change this line
// "jsx": "preserve", /* Specify what JSX code is generated. */
to look like this,
...
"jsx": "react",
...
// Also to avoid Typscript complaining about types please set strict mode to false."strict": false /* Enable all strict type-checking options. */,

Next, we will need the main file where our application code will go. For this we will useapp.tsx which will be our application and our React library both, and anhtml page where our React app will run. Lastly, we will need a local web-server to serve our page. For this, I created a very simple tsx and html page which are served viaserve (web-server) to finish setting things up.

// package.json"scripts": {
"dev": "tsc -w"
},
// index.html<div id="myapp"></div>
<script src="app.js"></script>
// app.tsx// This will our main app file.// ---- Library ---// ---- Application ---console.log('Hello World');// Open two terminals for the following commands respectively.npm run dev
npx serve .

This will start Typescript compilation as well as serve the HTML file from the web container, which when opened in a new window looks like this.

Congratulations!! 🎉🎉 You have done it, now it's time to have lots of fun. Basically, we have a webpage serving a typescript compiled file (in watch mode) with JSX support for React library. Now let’s get going with some hands-on coding.

👼 Our First JSX

Because we have JSX support, we can write something like this on our app.tsx (in fact, all our coding will happen on this file only, so if not mentioned you can assume I am referring to this file only).

const App = <div>Hello JSX!</div>;

If you look at the compilation screen, you will see something like this,

But we don’t have any React in the Code yet, why is it complaining about React not being there. To understand, let’s add our own great React library so that typescript has something to work with and we can see what’s going on.

// ---- Library ---const React = {};// ---- Application ---const App = <div>Hello JSX!</div>;

with this our compilation succeeds, as it's now found a reference to React. Let’s look at the compiled js code as well to see what it has generated for us.

Ah hah! we see how JSX is compiled by Typescript to generate the code React.createElement() . But our React is just a blank object yet, so this call must fail when we try to run this code on the browser, and sure enough, if we refresh the browser we see this.

createElement is not defined

So let’s define our createElement function in our React and for now just log what’s being passed to this function to see what’s happening.

// ---- Library --- //
const React = {
createElement: (...args) => {
console.log(args);
},

};
// ---- Application --- //
const App = React.createElement('div', null, 'Hello JSX!');

Sure enough, what we see are the args which look like HTML Tag, any props, and then any children for that element. Let’s modify our App to make it a little bit more comprehensive HTML to see what’s going on.

// ---- Application --- //
const App = (
<div draggable>
<h2>Hello React!</h2>
<p>I am a pargraph</p>
<input type="text" />
</div>
);

So, what do we see here, we see our createElement being called multiple times as per the structure of the JSX. And the order of the call is also such that top level elements are called after their children are called. Also, note that because we don’t have any children for input an element so it only has two arguments. I have added the draggable attribute on the top level div just to illustrate the props structure and how it's passed to us. Let’s see the generated code to reconfirm these observations.

Please note, here, that we also expectcreateElement to return something, as of now we are not returning anything only logging the args that’s why the children portions for the top-level element is like [undefined, undefined, undefined ]. So let’s fix that in the next section 🥶

🧚‍♂️ Introducing Virtual DOM 🧞‍♂️

DOM or Document Object Model is the official W3C way of specifying the structure of the HTML document we are working with which Browsers can understand and render. Virtual DOM, loosely speaking is a similar structure which can represent our page structure and its details but how to re-present it and how it should look like is completely upto to the implementor.

React Library mostly works in such Virtual DOM environment only and then hands over the representation to some renderer like ReactDOM for Web and ReactNative for Mobile which takes this Tree like structure and knows how to render it out to a WebPage or to a Native Mobile application.

For this post, let’s try to build our own representation which we can work with. We will keep it really simple, we will simply create an simple/plain JS Object when createElement is called keeping the tag, props and childrens and return that from createElement function. Take a look at the implementation below.

// ---- Library --- //
const React = {
createElement: (tag, props, ...children) => {
const el = {
tag,
props,
children,
};
console.log(el);
return el;
},

};

So what we did here is we split the args from ...args to explicitly receive all tag , props and children and specifically for children, collected them all in an array with accepting ...children . With this change our console looks something like this.

Our own Virtual DOM

This what Virtual DOM looks like, this structure which will form the basis of all other computation and rendering for our application is very important to get right.

👯 ReactElement vs ReactComponent

So far our App is just a direct DOM tree (somewhat), and its a called a ReactElement. When we would want to perform more complex operations within the DOM, like add a variable state etc., we can’t simply use Elements directly, we may want to wrap them in a function which is known more popularly as ReactComponent. Let’s try to do so in our app by modifying App to be a function and when the function is called it returns the same Virtual DOM tree we are familiar with so far. This involves changes in both App as well as the createElement function as it now needs to handle inputs like Components which are simply functions wrapping VirtualDOM instead of the actual VirtualDOM withtag , props and children we are familiar with so far.

// ---- Application --- //
const App = () => {
return (
<div draggable>
<h2>Hello React!</h2>
<p>I am a pargraph</p>
<input type="text" />
</div>
);
};
console.log(<App />);

Note, now, because tag is a function here and we are not handling function inputs yet in createElement So, it will not form the entire DOM tree and it will look something like this like this.

Please note here tag is a function type here see the () => {...} .

Now let’s handle, our createElement function to handle a ReactComponent. All we do is check the type of the first param ( tag in this case), if its a function we simply call the function so that it returns the required Elements from it.

// ---- Library --- //
const React = {
createElement: (tag, props, ...children) => {
if (typeof tag === 'function') {
return tag(props, ...children);
}

const el = {
tag,
props,
children,
};
return el;
},
};

With this change, our code is back in action constructing the Virtual DOM we aimed for.

React Component (VirtualDOM)

🧳 Let’s Render our VirtualDOM (renderer)🪖

Alright, we have come a long way in building our nice little React library. but so far everything is happening on console. Let’s now add the final piece to the puzzle which takes this tree representation and renders it out to the actual Browser.

Let’s think for a minute how will we go about doing that. Well, we will figure it out, for now we know we need a render function which takes a ReactComponent as argument and we will also need an actual DOM element where we want to render or mount this. For this, we have the one and only div element with id myapp where we want to render our App in our index.html file.

<div id="myapp"></div>  <-- This will be our mount point.
<script src="app.js"></script>

Ok, now we are ready to start defining out render function with this context in mind and see what it has as inputs to work with.

// ---- Library --- //
const render = (el, container) => {
console.log(el);
console.log(container);
};
// ---- Application --- //
render(<App />, document.getElementById('myapp'));

Great! we have our root node for the VirtualDOM(<App/> ) and actual DOM node where we want to mount our App . So, as browsers don’t understand our VirtualDOM, this function will start building out the Actual DOM tree from the VirtualDOM tree. we will use our Browser API to create DOM nodes and relations corresponding to the virtual DOM.

  • First, create the actual DOM node of type tag by calling document.createElement(tag) API.
  • Second, once a node is created, we will set all the props from the VirtualDOM node to the actual HTML node by looping overall props from the virtual node.
  • Third, if the node has any children, then we have to do the same above steps recursively and append the child to the current DOM(we just created) node as container for the childs.
  • Lastly, we wrap up by container.appendChild(domEl) which takes the node and appending it to the container it was supposed to in the Browser.
// ---- Library --- //
const render = (el, container) => {
// 1. First create the document node corresponding el
let domEl = document.createElement(el.tag);
// 2. Set the props on domEl
let elProps = el.props ? Object.keys(el.props) : null;
if (elProps && elProps.length > 0) {
elProps.forEach((prop) => (domEl[prop] = el.props[prop]));
}
// 3. Handle creating the Children.
if (el.children && el.children.length > 0) {
// When child is rendered, the container will be
// the domEl we created here.
el.children.forEach((node) => render(node, domEl));
}
// 4. append the DOM node to the container.
container.appendChild(domEl);
};

We are so close now, but if you run it, it will not work. Because, the only other thing we need to take care of is the case of elements (VirtualDOM nodes)that are not full-fledged DOM nodes but just text nodes. Before I solve for this try to see what happens when you try to run the above code.

Basically, to understand this error, try to see how the text elements are passed to render from VirtualDOM. Consider the <h2> element for example here. It has just a text as a child.

When render() is called with just 'Hello World' , the tag, props, and children properties are missing and we are trying to create an HTML node with document.createElement(el.tag) where el.tag is undefined and you can’t create a object out of undefined that’s what the above error is all about. So, let’s fix the last error here by handling the text nodes by simply creating actual text nodes in HTML and doing the same append to the container like above. (Also, for simple cases, TextNodes won’t have any children of their own so we can simply return from it )

// ---- Library --- //
const render = (el, container) => {
let domEl;
// 0. Check the type of el
// if string we need to handle it like text node.
if (typeof el === 'string') {
// create an actual Text Node
domEl = document.createTextNode(el);
container.appendChild(domEl);
// No children for text so we return.
return;
}

// 1. First create the document node corresponding el
domEl = document.createElement(el.tag);
// 2. Set the props on domEl
let elProps = el.props ? Object.keys(el.props) : null;
if (elProps && elProps.length > 0) {
elProps.forEach((prop) => (domEl[prop] = el.props[prop]));
}
// 3. Handle creating the Children.
if (el.children && el.children.length > 0) {
// When child is rendered, the container will be
// the domEl we created here.
el.children.forEach((node) => render(node, domEl));
}
// 4. append the DOM node to the container.
container.appendChild(domEl);
};

And with this change. Viola!! see our Browser, our app is rendered without Errors. Take a moment to congratulate yourself for all the hard work you have done so far. Really amazing, isn’t it.

React renderer in Action

🐸 Conclusion 🐳

Well, we started off with pretty much nothing as far as React is concerned. Then we learned how JSX is parsed and called. We created our own React which ultimately creates a Tree structure that we can work with. That’s our very own VirtualDOM, lastly, we wrote our own renderer which takes this VirtualDOM tree and converts it into an actual DOM Tree which the browser can render. In fact if you just look at the Application code, it looks pretty much like you are using React 💥💥, in fact, because we have a function (ReactComponent) we can use some variables here as well.

// ---- Application --- //
const App = () => {
const myName = 'Arindam';
return (
<div draggable>
<h2>Hello {myName}!</h2>
<p>I am a pargraph</p>
<input type="text" />
</div>
);
};
render(<App />, document.getElementById('myapp'));

Well, that’s where I would like to conclude for the first part. There are a lot of exciting things ahead of us like State Management, Concurrent Mode, SSR, etc. See you in the next sections! Happy Coding! ☃️☃️

You can find our code for the above discussion here.

--

--