Writing a reactive component system using virtual DOM

Folkert-Jan van der Pol
10 min readJun 21, 2019

--

Utilizing recursiveness in javascript to render virtual dom trees which can be rendered to the HTML DOM

Introduction

This article was mostly inspired by ‘How to write your own Virtual DOM’ by deathmood. All credits of this article go to him. My original understanding of this concept is thanks to his article.

It serves as an introduction to writing your own little template system utilizing a concept called Virtual DOM. This article came to be because I wanted to write down my mental model on how Virtual DOM works.

I hope you enjoy!

Let’s grasp the concept

To start, it’s probably a good idea to determine what’s meant by ‘Virtual DOM’. It’s clearly a two-part concept: it’s virtual, and has to do with the HTML DOM. Generally, in front-end development we interact with the HTML DOM via javascript in a number of ways. Editing attributes, adding classes, changing image sources etc.

Each of these interactions are pretty costly in terms of processing power: the less you update the DOM, the better. This is the reason reactive frameworks like Vue and React, which are used to apply changes in data to the DOM without reloading a page, are based around the idea of updating the DOM as dynamically and efficiently as possible. Imagine updates taking hundreds of seconds to show up on your screen. Bye, bye good user experience.

React (and partly Vue) solves this problem by using a Virtual DOM. The DOM elements are first build in javascript as an object. This object contains all the respectively necessary information:

  • The html tag of the element
  • Its attributes
  • Its children

This object is called a Virtual DOM Node, or node for short.

// Example of a virtual DOM node
{
type: 'li',
props: {
'class': 'list__item',
},
children: 'This is text inside the list item'
}

The combination of all html elements in a single object with smaller child objects, is called a Virtual DOM Tree, or tree for short.

// Example of a small DOM tree
{
type: 'ul',
props: {
'data-id': 14,
'class': 'list-unstyled to-do-list'
},
children: [
{type: li, ...}, // each of these children is a node itself
{type: li, ...}
]
}

Now, this Virtual DOM Tree, once our script is finished, will be rendered to the page like this:

// Output in HTML after rendering to the DOM
<ul class="list-unstyled to-do-list" data-id="14">
<li class="list__item">
This is text inside the list item
</li>
<li class="list__item">
This is text inside the list item
</li>
</ul>

So what’s the big deal you ask? Doesn’t seem to have any added benefit over just writing HTML in an HTML file so far.
Well, with a Virtual DOM we get access to all kinds of programmatic advantages. We can make elements for each entry in an array, and update the page accordingly if the array changes. For example, React uses a concept called diffing. In this process, any time data changes a new version of (part of the) tree is made and compared to the previous version. This way, exact differences in the tree structure or content can be pinpointed, and only these specific changes are handled and rendered to the HTML DOM. Et voila, a more efficient way of updating the HTML DOM without rendering the whole Virtual DOM Tree again.

Generating Virtual DOM Nodes & Trees

So how do we actually generate these so-called trees? We don’t want to hand write an object for the page structure.
The basic functionality of the Virtual DOM consists of two types of functions:

  • The virtualizer: generates the virtual nodes
  • The creator: turns the virtual nodes into DOM elements

The virtualizer

Let’s take a look at the virtualizer first:

const virtualize = (type, props, ...children) => {
return {type, props, children}
}

That doesn’t look too bad! What’s the big deal?
The strength of this function comes in numbers. Utilizing this function we can make an HTML DOM like template that is clear to read. Let’s make the function structure that outputs the Virtual DOM Tree for the list from the previous examples:

const v = virtualize // i like to shorten the function name for template clarityconst listTemplate = v('ul', {'class': ..., 'data-id': 14},
v('li', {...}, 'This is text inside the list item'),
v('li', {...}, 'This is text inside the list item'),
)

As seen above, you always start with the parent. You pass its element tag, props and then the interesting part is the children: for each child the node has, you just pass an extra virtualize function as a parameter.
Since ES6, we can use the spread operator to spread out all extra parameters into one array labeled children.

virtualize = (type, props, ...children) => {}

The output of the now made list template is as follows:

console.log( listTemplate )// expected output:
{
type: 'ul',
props: {
'data-id': 14,
'class': 'list-unstyled to-do-list'
},
children: [
{type: li, ...},
{type: li, ...}
]
}

BOOM. We have a Virtual DOM Tree.

The creator

Now let’s actually create the elements this collection of Virtual Nodes represents.
Let’s first write the logic for turning the type of a Virtual Node into an HTML element.

The function below accepts a virtual node and returns an HTML element. The element has to be returned, so later on in the script it can be actually rendered inside an element in the DOM.

const create = (node) => {
const {type, props, children} = node
const el = document.createElement(type) return el
}

Great! Seems simple enough. But wait, it doesn’t end here. This function makes just one virtual node into its respective HTML element. If we insert the Virtual DOM Tree from the previous examples, it would just create an empty unordered list tag. We want the list items!

Here, it gets a little more complicated. We want to render a node, but also its children. We don’t want to write specific functions to render children. Great news: we don’t need to!
Each of the children of a node, is a node in itself. That sounds like a job for recursion!
For those of you that encounter recursion for the first time, recursion is basically a function calling itself over and over again till it doesn’t need to do so anymore. You can read the (technical) description in this article.
Let’s add support for rendering children to our creator, by looping through a node’s children and calling the create function for each of them.

const create = (node) => {
const {type, props, children} = node
const el = document.createElement(type) children
.map(child => create(child))
return el
}

Yay! All nodes will turn into HTML elements!
There is a problem however: we lost our nested structure. The elements will be created, but they will all be regarded as siblings. They don’t remember they had a parent at all.

To solve this, we can simply loop over the array of children again and append them to the original parent:

const create = (node) => {
const {type, props, children} = node
const el = document.createElement(type) children
.map(child => create(child))
.forEach(el.appendChild.bind(el))
return el
}

Yay! Nested HTML elements!
This however, will throw some errors. Some Virtual Nodes don’t have other nodes as children, but have text as a child. We need to accommodate for this situation and just return a text node if we try to create a node that’s just a string. This will automatically be appended to the element it’s supposed to be in, because of the way the recursion takes place.

const create = (node) => {
if (typeof node === 'string') {
return document.createTextNode(node)
}
const {type, props, children} = node const el = document.createElement(type) children
.map(create) // short for .map(child => create(child))
.forEach(el.appendChild.bind(el))
return el
}

Yaaaaay! We have nested HTML elements with proper content! Let’s see the output by using our previously made Virtual DOM Tree, the listTemplate:

body.appendChild(
create(listTemplate)
)
// expected HTML output appended to body:
<ul>
<li>This is text inside the list item</li>
<li>This is text inside the list item</li>
</ul>

Our next step is to make sure that our props are set. It would be a shame if we couldn’t render our classes or other attributes. For clarity, let’s make this a different function. Let’s create a function that will be called in the creator function, and takes the freshly made html element and the props it needs:

const setProps = (el, props) => {
}

Now, for each prop we need to add it one by one to the element. Let’s write a loop that goes through the props object utilizing Object.entries, so we can split up the object’s keys and values.

const setProps = (el, props) => {
for (const [key, value] of Object.entries(props)) {
// set single prop
}

}

Now we can just add the prop to the element with setAttribute:

const setProps = (el, props) => {
for (const [key, value] of Object.entries(props)) {
el.setAttribute(key, value)
}
}

Let’s add a call to this function in the creator, so props are set when a Virtual Node is just turned into an HTML element:

const create = (node) => {
if (typeof node === 'string') {
return document.createTextNode(node)
}
const {type, props, children} = node const el = document.createElement(type)

setProps(el, props)
children
.map(create)
.forEach(el.appendChild.bind(el))
return el
}

Now if we create our listTemplate once more, the output is as follows:

body.appendChild(
create(listTemplate)
)
// expected ouput:
<ul class="list-unstyled to-do-list" data-id="14">
<li class="list__item">This is text inside of the list item</li>
<li class="list__item">This is text inside of the list item</li>
</ul>

Congratulations! You just correctly rendered your first Virtual DOM Tree!

Some programmatic examples

Let’s make another list. This time, it’s based on some data. For each entry in the data array, I want to render a list item in an unordered list. This is how that would look like:

const v = virtualize
const data = ['item 1', 'item 2', 'item 3']
const listTemplate = v(
'ul', {},
...data.map(entry => {
return v('li', {}, entry)
})
)
body.appendChild(
create(listTemplate)
)

The expected output of the script above should be:

<ul>
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
</ul>

Let’s spice things up

So what about some kind of component system, where you can have a function fire the moment the Virtual Node is rendered to the page?

Let’s write a basic Class for components, giving it the virtualize function to later make a components associated Virtual DOM Tree:

class Component {
constructor() {
this.virtualize = virtualize
}
}

Our first component

Let’s make an empty to-do list component:

class ToDoList extends Component {
constructor(props) {
super(props)
}
}

We are going to add some functionality to this component. First, we’ll add the function that returns its Virtual DOM Tree.

const items = ['item 1', 'item 2', 'item 3']class ToDoList extends Component {
constructor(props) {
super(props)
}
build() {
const v = this.virtualize
return v('ul', {class: 'to-do-list', 'data-id': 14},
...items.map(item => v('li', {}, item))
)
}

}

Now, if we want to make a to-do list, we simply call its build method:

const ToDoListOne = new ToDoList()body.appendChild(
create(ToDoListOne.build())
)
// expected HTML output:
<ul class="to-do-list" data-id="14">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
</ul>

Let’s add a mounted function

Let’s add a function that adds a listener to the list. When you click the list, its data-id value should be printed in the console. This can only work, if the mounted function fires after the Virtual DOM Tree is already rendered on the page.

const items = ['item 1', 'item 2', 'item 3']class ToDoList extends Component {
constructor(props) {
super(props)
}
build() {
const v = this.virtualize
return v('ul', {class: 'to-do-list', 'data-id': 14},
...items.map(item => v('li', {}, item))
)
}
mounted() {
const list = document.querySelector('.to-do-list')
list.addEventListener('click', () => {
console.log(list.dataset.id)
}
}

}

Let’s call that!

const ToDoListOne = new ToDoList()body.appendChild(
create(ToDoListOne.build())
)
ToDoListOne.mounted() // output: 14

Okay, this doesn’t seem to exciting. We have to write every instance down, append it to the body and call its mounted function. We want this to be more dynamic. And what if we want to have components within components within trees?

Adding support for classes to the creator

Let’s add something to our create function. If a class is passed into the create function, we want to:

  • Render its Virtual DOM Tree to the page
  • Call its mounted function when done

So, let’s add support for that:

const create = (node) => {
if (typeof node === 'string') {
return document.createTextNode(node)
}
if (typeof node === 'function' || typeof node.type === 'function')
{
// a class is a function in js
const c = new type() // make a new instance of class
const el = create(c.build()) // render the Virtual DOM Tree
setTimeout(() => c.mounted(), 0) // throw mounted to bottom of call stack to make sure it always fires after tree is rendered
return el
}
const {type, props, children} = node const el = document.createElement(type) children
.map(create)
.forEach(el.appendChild.bind(el))
return el
}

So now, we can just start rendering the Virtual DOM Tree with the way we used to render it:

body.appendChild(
create(ToDoList)
)
// expected output:
<ul class="to-do-list" data-id="14">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
</ul>
// when clicking on the list, the console will print:
14

Now we have structures based on a class! Seems all well and good. Let’s see the impact of that. Let’s create a component called Subject. Each Subject will have a template of its own, with a to-do list inside of it.

class Subject extends Component {
constructor(props) {
super(props)
}
build() {
const v = this.virtualize
return v('article', {class: 'subject'},
v('h3', {}, 'A subject'),
v(ToDoList)
)
}
}

If we create this component, the output will look like this:

body.appendChild(
create(Subject)
)
// expected HTML output:
<article class="subject">
<h3>A subject</h3>
<ul class="to-do-list" data-id="14">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
</ul>
</article>

Final words

Of course, there’s a lot more that goes into making a fully working framework. There’s still a lot missing, like diffing, reactivity, one-way or two-way binding etc. This article just serves the purpose of showing a basic entry into the world of Virtual DOMs. It possibly provides a new mental model for how parts of frameworks like React work under the hood.

Furthermore, it’s fun to prototype with your ‘own’ Virtual DOM component system. When starting to prototype, you keep adding and adding functionality to class components. Some ideas are to add a store (like Vue’s Vuex or React’s Redux), a router etc. The possiblities are endless!

Remember, I do not claim the concept of Virtual DOM as my own. It’s a well known principle, exceptionally explained by deathmood in his article.

--

--