Learning JavaScript by building a UI framework from scratch
Tags: javascript, frameworks, programming, virtualdom
Published: 2019-05-01 15:48:49

---
title: Learning JavaScript by building a UI framework from scratch
published: true
description: A first principles approach to learning JavaScript
tags: javascript, frameworks, programming, virtual-dom 
---

In my [previous](https://dev.to/carlmungazi/turning-class-app-extends-react-component-into-a-coding-lesson-3mod) post I explained how APIs from your favourite libraries and frameworks can be turned into programming lessons. Today I will develop that idea by taking it a step further. We will not read other people's code. Instead, we will write our own and build a very basic and rudimentary UI framework.

Building a framework is a good way to deepen your JavaScript and programming knowledge because it forces you to explore language features and software engineering principles. For example, all web frameworks try to solve the problem of keeping application data in sync with the UI. All the solutions to this problems can touch different areas such as routing, DOM manipulation, state management and asynchronous programming.

One of the more popular ways of solving this UI-state-sync problem is using a concept known as the **virtual DOM** (or vdom). Instead of manipulating the DOM directly in response to data changes, we can use JavaScript objects because they are computationally much cheaper to manipulate. The vdom approach can be broken down like so:

1. When your application is first loaded, create a tree of JavaScript objects which describe your UI
2. Turn these objects into DOM elements using DOM APIs such as `document.createElement`
3. When you need to make a change to the DOM (either in response to user interaction, animations or network requests), create another tree of JavaScript objects describing your new UI
4. Compare the old and new tree of JavaScript objects to see which DOM elements have been changed and how
5. Make changes to the DOM only in places that have changed


One of the fundamental pieces of any vdom implementation is the function which creates the object. Essentially, this function must return an object containing the information needed to create a DOM element. For instance, in order to create this DOM structure:

```html
<ul class="list">
    <li class="list-item" style="color: red;">Item 1</li>
    <li class="list-item" style="color: blue;">Item 2</li>
</ul>
```

You need to know the following information for each DOM element:
- type of element
- list of attributes
- if it has any children (for each child, we also need to know the same information listed above)

This leads us to our first lesson: **data structures**. As Linus Torvalds said, "Bad programmers worry about the code. Good programmers worry about data structures and their relationships". So how can we represent the DOM structure above in code? 

```js
{
  type: 'ul',
  attrs: {
      'class': 'list'
  },
  children: [
    {
      type: 'li',
      attrs: {
        class: 'list-item',
        style: {
          color: 'red'
        }
      },
    },
    {
      type: 'li',
      attrs: {
        class: 'list-item',
        style: {
          color: 'blue'
        }
      },
    } 
  ]
}

```

We have an object with three properties and each property is either a `string`, `object` or `array`. How did we choose these data types? 

- All HTML elements can be represented by a string
- HTML attributes have a `key: value` relationship which lends itself nicely to an object
- HTML child nodes can come in a list format and creating them requires performing the same operation on each item in the list. Arrays are perfect for this

So now we know what our data structure looks like, we can move on to the function which creates this object. Judging by our output, the simplest thing to do would be to create a function with takes three arguments.

```js
createElement (type, attrs, children) {
  return {
    type: type,
    attrs: attrs,
    children: children
  }
}
```

We have our function but what happens if when invoked, it does not receive all the arguments? Furthermore, does the creation of our object require every argument to be present?

This leads us to the next lesson: **error handling**, **default parameters**, **destructuring** and **property shorthands**.

Firstly, you cannot create a HTML element without specifying a type, so we need to guard against this. For errors, we can borrow Mithril's approach of [throwing](https://github.com/MithrilJS/mithril.js/blob/next/render/hyperscript.js#L87) an error. Alternatively, we can define custom errors as described [here](https://medium.com/@iaincollins/error-handling-in-javascript-a6172ccdf9af). 

```js
createElement (type, attrs, children) {
  if (type == null || typeof type !== 'string') {
    throw Error('The element type must be a string');
  }

  return {
    type: type,
    attrs: attrs,
    children: children
  }
}
```

We will revisit this check `type == null || typeof type !== 'string'` later on but for now, let us focus on creating our object. Whilst we cannot create HTML elements without specifying a type, we can create HTML elements that have no children or attributes. 

In JavaScript, if you call a function without providing any of the required arguments, those arguments are assigned the value `undefined` by default. So `attrs` and `children` will be `undefined` if not specified by the user. We do not want that because, as we will see later, the rest of our code expects those arguments to contain a value. To solve this, we will assign `attrs` and `children` default values:

```js
createElement (type, attrs = {}, children = []) {
  if (type == null || typeof type !== 'string') {
    throw Error('The element type must be a string');
  }

  return {
    type: type
    attrs: attr,
    children: children
  }
}
```

As mentioned earlier, HTML elements can be created without any children or attributes, so instead of requiring three arguments in our function, we can require two:

```js
createElement (type, opts) {
  if (type == null || typeof type !== 'string') {
    throw Error('The element type must be a string');
  }

  return {
    type: type
    attrs: opts.attr,
    children: opts.children
  }
}
```

We have lost the default parameters introduced earlier but we can bring them back with destructuring. Destructuring allows us to unpack object properties (or array values) and use them as distinct variables. We can combine this with shorthand properties to make our code less verbose.  

```js
createElement (type, { attrs = {}, children = [] }) {
  if (type == null || typeof type !== 'string') {
    throw Error('The element type must be a string');
  }

  return {
    type,
    attrs,
    children
  }
}
```

Our function can create virtual dom objects but we are not done yet. Earlier we skipped over this bit of code `type == null || typeof type !== 'string'`. We can now revisit it and learn something else: **coercion**. 

There are four things to observe here:
- the behaviour of the `==` loose equality operator
- the behaviour of the `||` operator
- the behaviour of `typeof` operator
- the behaviour of `!==` operator

When I first learnt JavaScript I came across numerous articles advising against using the loose equality operator. This is because it produces surprising results such as:

```js
1 == '1' // true
null == undefined // true
```

It is surprising because in the examples above, we are comparing values of four different primitive types: `number`, `string`, `null` and `undefined`. The checks above evaluate to `true` because `==` performs a coercion when comparing values of differing types. The rules that govern how this happens can be found [here](http://www.ecma-international.org/ecma-262/#sec-abstract-equality-comparison). For our specific case, we need to know the spec states that `null == undefined` will always return true. Also, `!==` works by performing the same checks performed by `===` and then negating the result. You can read the rules about that [here](http://www.ecma-international.org/ecma-262/#sec-strict-equality-comparison).

Returning to our function, the first thing this `type == null || typeof type !== 'string'` is checking is if a `null` or `undefined` value has been passed. Should this be `true`, the `||` operator will return the result of `typeof type !== 'string'`. The order of how this happens is important. The `||` operator does not return a `boolean` value. It returns the value of one of the two expressions. It first performs a `boolean` test on `type == null`, which will either be `true` or `false`. If the test returns `true`, our error would be thrown. 

However, if `false` is returned, `||` returns the value of the second expression, which in our case will either be `true` or `false`. If our check had been `type == null || type` and the first expression resulted in `false`, the second expression would return whatever value is in the variable `type`. The `typeof` operator returns a string indicating the type of the given value. We did not use it for our `type == null` check because `typeof null` returns `object`, which is an [infamous](http://2ality.com/2013/10/typeof-null.html) bug in JavaScript.

With that newfound knowledge, we can take a harder look at `createElement` and ask ourselves the following questions: 
- How do we check that the second argument can be destructed?
- How do we check that the second argument is an object?

Let us start by invoking our function with different argument types:

```js
createElement (type, { attrs = {}, children = [] }) {
  if (type == null || typeof type !== 'string') {
    throw Error('The element type must be a string');
  }

  return {
    type,
    attrs,
    children
  }
}

createElement('div', []); // { type: "div", attrs: {…}, children: Array(0) }
createElement('div', function(){}); // { type: "div", attrs: {…}, children: Array(0) }
createElement('div', false); // { type: "div", attrs: {…}, children: Array(0) }
createElement('div', new Date()); // { type: "div", attrs: {…}, children: Array(0) }
createElement('div', 4); // { type: "div", attrs: {…}, children: Array(0) }

createElement('div', null); // Uncaught TypeError: Cannot destructure property `attrs` of 'undefined' or 'null'
createElement('div', undefined); // Uncaught TypeError: Cannot destructure property `attrs` of 'undefined' or 'null'
```

Now we modify the function:

```js
createElement (type, opts) {
  if (type == null || typeof type !== 'string') {
    throw Error('The element type must be a string');
  }

  if (arguments[1] !== undefined && Object.prototype.toString.call(opts) !== '[object Object]') { 
    throw Error('The options argument must be an object'); 
  }
  
  const { attrs = {}, children = [] } = opts || {};

  return {
    type,
    attrs,
    children
  }
}

createElement('div', []); // Uncaught Error: The options argument must be an object
createElement('div', function(){}); // Uncaught Error: The options argument must be an object
createElement('div', false); // Uncaught Error: The options argument must be an object
createElement('div', new Date()); // Uncaught Error: The options argument must be an object
createElement('div', 4); // Uncaught Error: The options argument must be an object

createElement('div', null); // Uncaught Error: The options argument must be an object
createElement('div', undefined); // Uncaught Error: The options argument must be an object
```

Our first function was not fit for purpose because it accepted values of the wrong type. It also gave us a `TypeError` when invoked with `null` or `undefined`. We fixed this in our second function by introducing a new check and new lessons: **error types**, **rest parameters** and **this**. 

When we invoked the function with `null` or `undefined` as the second argument, we saw this message: `Uncaught TypeError: Cannot destructure property 'attrs' of 'undefined' or 'null'`. A `TypeError` is an object which represents an error caused by a value not being the expected type. It is one of the more common error types along with `ReferenceError` and `SyntaxError`. This is why we reverted to using an object as our argument because there is [no](https://stackoverflow.com/questions/55638953/how-to-handle-null-and-undefined-values-when-destructuring-function-argument) way of guarding against `null` and `undefined` values when destructuring function arguments.

Let us take a closer look at the check in our second iteration: 

```js
if (arguments[1] !== undefined && Object.prototype.toString.call(opts) !== '[object Object]') { 
  throw Error('The options argument must be an object'); 
}
```

The first question to ask is: why are we using the arguments object when [rest parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters) are a thing? Rest parameters were introduced in ES6 as a cleaner way of allowing developers to represent an indefinite number of arguments as an array. Had we used them, we could have written something like this:

```js
createElement (type, ...args) {
  if (type == null || typeof type !== 'string') {
    throw Error('The element type must be a string');
  }

  if (args[0] !== undefined && Object.prototype.toString.call(args[0]) !== '[object Object]') { 
    throw Error('The options argument must be an object'); 
  }
}
```

This code is useful if our function had many arguments but because we are only expecting two, the former approach works better. The more exciting thing about our second function is the expression `Object.prototype.toString.call(opts) !== '[object Object]'`. That expression is one of the answers to the question: *In JavaScript, how do you check if something is an object?* The obvious solution to try first is `typeof opts === "object"` but as we discussed earlier, it is not a reliable check because of the JavaScript bug that returns `true` using `typeof` with `null` values.

Our chosen solution worked in the ES5 world by taking advantage of the internal `[[Class]]` property which existed on built-in objects. According to the ES5 [spec](http://www.ecma-international.org/ecma-262/5.1/#sec-8.6.2), this was a *string value indicating a specification defined classification of objects*. It was accessible using the `toString` method. The [spec](http://www.ecma-international.org/ecma-262/5.1/#sec-15.2.4.2) explains `toString`'s behaviour in-depth but essentially, it returned a string with the format `[object [[Class]]]` where `[[Class]]` was the name of the built-in object. 

Most built-ins overwrite `toString` so we have to also use the `call` method. This method calls a function with a specific `this` binding. This is important because whenever a function is invoked, it is invoked within a specific context. JavaScript guru Kyle Simpson has [outlined](https://github.com/getify/You-Dont-Know-JS/blob/master/this%20%26%20object%20prototypes/ch2.md#review-tldr) the four rules which determine the order of precedence for `this`. The second rule is that when a function is called with `call`, `apply` or `bind`, the `this` binding points at the object specified in the first argument of `call`, `apply` or `bind`. So `Object.prototype.toString.call(opts)` executes `toString` with the `this` binding pointing at whatever value is in `opts`. 

In ES6 the `[[Class]]` property was removed so whilst the solution still works, its behaviour is slightly [different](http://www.ecma-international.org/ecma-262/9.0/index.html#sec-object.prototype.tostring). The spec advises against this solution, so we could seek inspiration from [Lodash](https://github.com/lodash/lodash/blob/4.17.11/lodash.js#L3069)'s way of handling this, for example. However, we will keep it because the risk of it producing erroneous results are very low.

We have created what on the surface appears to be a small and basic function but as we have experienced, the process is anything but small or basic. We can move on to the next stage but that leads to the question, what should that stage be? Our function could do with some tests but that would require creating a development workflow. Is it too early for that? If we add tests, which testing library are we going to use? Is it not better to create a working solution before doing any of this other stuff? These are the kind of tensions developers grapple with daily and we will explore those tensions (and the answers) in the next tutorial.