Building a React-Like Library: Static Virtual DOM
Building a React-like library to learn how frontend frameworks works under the hood.
Building a React-Like Library
This is the first of four issues where we build a React-like library to learn how frontend frameworks work under the hood.
The series contains the following articles:
Render a static virtual DOM. This article.
Functional Components. Upcoming.
Implement useState. Upcoming.
Implement diffing. Upcoming.
Introduction
HTML is a markup language that defines the structure of a website. The DOM is the representation of the objects defined by the HTML. The browser creates and manages the DOM internally.
Virtual DOM is when this representation lives in the Javascript code.
Note: For more details, I wrote an article on HTML, DOM, Shadow DOM, and Virtual DOM.
Virtual DOM is a pattern many frontend frameworks use to render applications. Understanding the virtual DOM helps us understand the frameworks at a deeper level.
Initial Example
Let’s start with a simple HTML:
<div>
<h1 id=”title”>Hello, World!</h1>
<p>I’m learning about virtual DOM</p>
</div>
Below is a tree representation of this HTML:
A representation of this DOM in JS is:
const vDom = {
tag: "div",
props: {},
children: [
{
tag: "h1",
props: { id: "title" },
children: ["Hello, World!"]
},
{
tag: "p",
props: {},
children: ["I’m learning about virtual DOM"]
}
]
};
Render the Virtual DOM
To render this virtual DOM, we’ll need a function that receives this tree as input and manipulates the DOM accordingly.
render(vDom);
We also need to know where to render it. Most single-page applications start with an HTML and an empty div
used by the render
function. Something like the following:
<html>
<!-- ... -->
<body>
<div id=”root”></div>
<script src=”./app.js”></script>
<body>
</html>
It’s the responsibility of app.js
to populate the <div id=”root”></div>
.
// app.js
const vDom = {...}
render(vDom, document.getElementById(“root”));
Virtual Nodes
Before we implement the render
function, we need to understand the data of the virtual DOM.
The previous virtual DOM uses a simple structure to represent an HTML element:
{
tag: “h1”,
props: { id: “title” },
children: ["Hello, World!"]
}
“tag”: the tag name of the element, such as “div” or “h1”.
“props”: an object with the attributes of the element. For example,
{ id: ‘title’ }
.“children”: the nodes that go between the opening and closing tag in the HTML representation
<div>(children)</div>
.
A node can also be a string.
“Hello, World!”
With these representations of HTML elements, we have all we need to display them.
The Render Function
Let’s write the render
function:
// app.js
const render = (vnode, parent) => {
// PENDING
}
Node as Object
A node is an object with the tag
property:
const render = (vnode, parent) => {
// create HTML element
const element = document.createElement(vnode.tag);
}
The attributes are in the props
:
const render = (vnode, parent) => {
// ...
// set attributes
if (vnode.props) {
Object.keys(vnode.props).forEach(key => {
const value = vnode.props[key];
element.setAttribute(key, value);
});
}
}
Our virtual DOM starts with one node:
{
tag: “div”,
props: {},
children: [...]
}
The rest of the elements are children of this first node. We also need to render them:
const render = (vnode, parent) => {
// ...
// iterate over children and render them
if (vnode.children) {
// call render recursively
vnode.children.forEach(child => render(child, element));
}
}
And finally, we need to append the element we created to the parent.
const render = (vnode, parent) => {
// ...
// append the element created
return parent.appendChild(element);
}
If we run this, we almost get what we want:
<div>
<h1 id=”title”></h1>
<p></p>
</div>
Node as String
We still need to manage a last case: when the node is just a string:
children: ["Hello, World!"]
children: ["I’m learning about virtual DOM"]
In both of these cases, the render function is called with the string:
// `child` is just `"Hello, World!"`
vnode.children.forEach(child => render(child, element));
Therefore, we need to add this extra check before we create the element:
const render = (vnode, parent) => {
// manage string type
if (typeof vnode === "string") {
return parent.appendChild(document.createTextNode(vnode));
}
// ...
}
If we put it all together, we have the following render
function:
const render = (vnode, parent) => {
// manage string type
if (typeof vnode === "string") {
return parent.appendChild(document.createTextNode(vnode));
}
// create HTML element
const element = document.createElement(vnode.tag);
// set attributes
if (vnode.props) {
Object.keys(vnode.props).forEach(key => {
const value = vnode.props[key];
element.setAttribute(key, value);
});
}
// iterate over children and render them
if (vnode.children) {
vnode.children.forEach(child => render(child, element));
}
// append the element created
return parent.appendChild(element);
};
render(vDom, document.getElementById(“root”));
Simple Single-Page Application
We built a simplified version of how frameworks like React render applications using a virtual DOM.
Find all the code in this gist.
The power of the virtual DOM comes further down the road. Stay tuned for the next article!
Thanks to Sebastià and Michal for reviewing this article 🙏
Share if you like this post!
Don’t forget to subscribe if you want to learn how frontend frameworks work under the hood.