We’ll start by abstracting away the nitty-gritty DOM manipulation code required for view creation. Enter the concept of the virtual DOM. With a virtual DOM tree, you can define how a view should appear and let the framework handle the heavy lifting, using the DOM API to create its representation in the browser.
In chapter 5, you’ll dive into crafting a state management system that communicates changes to your framework whenever the application state is altered. By bringing together the function responsible for rendering the virtual DOM and the state management system, you’ll have the initial version of your own framework.
Chapter 6 takes you one step further as you publish your framework on NPM
and use it to enhance your TODO application. That’s exciting!
Before we conclude this part, we’ll tackle a critical algorithm that underpins virtual-DOM-based frameworks: the reconciliation algorithm. Although the concept is complex, I’ll guide you through it step by step. This algorithm ensures that only the portions of the DOM that require updates are modified, minimiz-ing unnecessary changes.
An incredible adventure lies ahead—one that I embarked on with great enthusiasm. I hope that you’ll find this journey as exhilarating as I did!
Rendering and
the virtual DOM
This chapter covers
Defining the virtual DOM
Understanding the problems the virtual DOM
solves
Implementing functions to create virtual DOM
nodes
Defining the concept of a stateless component
As you saw in chapter 2, mixing application and Document Object Model (DOM) manipulation code gets unwieldy quickly. If for every event resulting from the user’s interaction with the application, we have to implement not only the business logic—the one that gives value to the application—but also the code to update the DOM, the codebase becomes a hard-to-maintain mess. We’re mixing two different levels of abstraction: the application logic and the DOM manipulation. What a maintenance nightmare!
Manipulating the DOM results in imperative code—that is, code that describes how to do something step by step. By contrast, declarative code describes what to do without specifying how to do it; those details are implemented somewhere else. Also, manipulating the DOM is a low-level operation—that is, it requires considerable knowledge of 43


44
CHAPTER 3
Rendering and the virtual DOM
the Document API and sits below the application logic. Contrast this operation with higher-level application code, which is framed in a language that is close to the business so that anyone working on the project can—and should—understand.
We would have a much cleaner codebase if we could describe in a more declarative manner how we want the view of our application to look and let the framework manipulate the DOM to create that view. What we need is similar to blueprints for a house, which describe what has to be built without specifying how to build it. The architect designs the blueprints and lets the construction company take care of building the house. Suppose that instead, the architect had to not only design the house, but also go to the construction site and build it—or at least tell the construction workers how they need to do their jobs step by step without missing a single detail. That approach would be a very inefficient way to build.
For the sake of productivity, the architect focuses on what needs to be built and lets the construction company take care of how it’s built. Similarly, we want the application developer to focus on the “what” (what the view should look like) and let the framework take care of the “how” (how to assemble the view using the Document API).
NOTE
You can find all the listings in this chapter in the listings/ch03 directory of the book’s repository The code you write in this chapter is for the framework’s first version, which you’ll publish in chapter 6. Therefore, the code in this chapter can be checked out from the ch6
label (): $ git switch --detach ch6.
3.1
Separating concerns: DOM manipulation vs.
application logic
In chapter 2, you wrote all the code together as part of the application, as you see in figure 3.1. That code is in charge of initializing the application and its state, programmatically building the view using the Document API, and handling the events that result from the user’s interactions with the application by modifying the DOM accordingly.
Initialize the app and state.
Draw the view using the Document API.
Handle the events by
js
1. Updating the state.
Application
code
2. Modifying the DOM.
Figure 3.1
So far, all the code is written together as part of the application.



3.1
Separating concerns: DOM manipulation vs. application logic
45
In this chapter, we want to separate the code that describes the view (the application’s code) from the code that uses the Document API to manipulate the DOM and create the view (the framework’s code). A term widely used in the software industry describes this process: separation of concerns.
DEFINITION
Separation of concerns means splitting the code so that the parts that carry out different responsibilities are separated, which helps the developer understand and maintain the code.
Figure 3.2 shows the separation of concerns we want to achieve: splitting the application code from the framework code that deals with DOM manipulation and keeps track of the state. We’ll focus on rendering the view in this chapter and chapter 4; we’ll leave the state management for chapter 5.
Initialize the app and state.
Describe how the view should look.
js
Handle the events by
Application
1. Updating the state.
code
Draw the view using the Document API.
js
When the state changes, modify the DOM.
Framework
code
Figure 3.2
By the end of chapter 4, you’ll have separated the code that describes the view from the code that manipulates the DOM.
The main objective of this separation of concerns is to simplify the application developer’s job: they need to focus only on the application logic and let the framework take care of DOM manipulation. This approach has three clear benefits:
Developer productivity—The application developer doesn’t need to write DOM-manipulation code; instead, they can focus on the application logic. They have to write less code, which enables them to ship the application faster.
Code maintainability—The DOM manipulation and application logic aren’t mixed, which makes the code more succinct and easier to understand.
Framework performance —The framework author, who’s likely to understand how to produce efficient DOM-manipulation code better than the application developer does, can optimize how the DOM is manipulated to make the framework more performant.
46
CHAPTER 3
Rendering and the virtual DOM
Going back to the blueprints analogy, how do you define what the view should look like, as the architect does with blueprints? The answer is the virtual DOM.
3.2
The virtual DOM
The word virtual describes something that isn’t real but mimics something that is. A virtual machine (VM), for example, is software written to mimic the behavior of a real machine—hardware. A VM gives you the impression that you’re running a real machine, but it’s software running on top of your computer’s hardware.
DEFINITION
The virtual DOM is a representation of the actual DOM
in the browser. The DOM is an in-memory tree structure managed by the browser engine, representing the HTML structure of the web page. By contrast, the virtual DOM is a JavaScript-based in-memory tree of virtual nodes that mirrors the structure of the actual DOM. Each node in this virtual tree is called a virtual node, and the entire construct is the virtual DOM.
I’ll use vdom as an abbreviation of virtual DOM in the code listings.
The nodes in the actual DOM are heavy objects that have hundreds of properties, whereas the virtual nodes are lightweight objects that contain only the information needed to render the view. Virtual nodes are cheap to create and manipulate. Suppose that we want to produce the following HTML:
<form action="/login" class="login-form">
<input type="text" name="user" />
<input type="password" name="pass" />
<button>Log in</button>
</form>
The HTML consists of a <form> with three child nodes: two <input> and a <button>.
A virtual DOM representation of this HTML needs to contain the same information as the DOM:
What nodes are in the tree and their attributes
The hierarchy of the nodes in the tree
The relative positions of the nodes in the tree
It’s important that the virtual DOM includes the <form> as the root node, for example, and that the two <input> and <button> are its children. The form has action and class attributes, and the button has an onclick event handler, although it’s not visible in the HTML. The type and name attributes of the <input> elements are crucial: an <input> of type text and an <input> of type password aren’t the same; they behave differently. Also, the relative position of the form’s children is important: the button should go below the inputs. The framework needs all this information to render the view correctly. Following is a possible virtual DOM representation of this HTML made of pure JavaScript objects:
3.2
The virtual DOM
47
{
type: 'element',
tag: 'form',
props: { action: '/login', class: 'login-form' },
children: [
{
type: 'element',
tag: 'input',
props: { type: 'text', name: 'user' }
},
{
type: 'element',
tag: 'input',
props: { type: 'password', name: 'pass' }
},
{
type: 'element',
tag: 'button',
props: { on: { click: () => login() } },
children: [
{
type: 'text',
value: 'Log in'
}
]
}
]
}
Each node in the virtual DOM is an object with a type property that identifies what kind of node it is. This example has two types of nodes:
element—Represents a regular HTML element, such as <form>, <input>, or
<button>
text—Represents a text node, such as the 'Log in' text of the <button> element in the example
We’ll see one more type of node later in the chapter: the fragment node, which groups other nodes together but has no semantic meaning of its own. Each type of node has a set of properties to describe it. Text nodes, for example, have one property apart from the type: value, which is the string of text. Element virtual nodes have three properties:
tag—The tag name of the HTML element.
props—The attributes of the HTML element, including the event handlers inside an on property.
children—The ordered children of the HTML element. If the children array is absent from the node, the element is a leaf node.
As we’ll see, fragment nodes have a children array of nodes, similar to the children array of element nodes. Using this virtual DOM representation allows the developer to describe what the view of their application—the rendered HTML—should look like.
48
CHAPTER 3
Rendering and the virtual DOM
You, the framework author, implement the code that takes that virtual DOM representation and builds the real one in the browser. This way, you effectively separate the code that describes the view from the code that manipulates the DOM.
We can represent the virtual DOM in the previous example graphically as a tree, as shown in figure 3.3. The <form> element is the root node of the tree, and the two
<input> and <button> elements are its children. The properties of each node, such as the action and class attributes of the <form> element, are inside the node’s box. The
<button> element has a child node—a text node with the text "Log in".
<form>
class: "login-form"
action: "login"
<input>
<input>
<button>
type: "text"
type: "password"
click: login()
name: "user"
name: "pass"
Text
"Log in"
Figure 3.3
The virtual DOM is a representation of the DOM made of JavaScript objects.
NOTE
As you can see in figure 3.3, the nodes of the tree have a title indicat-ing their type. The HTML elements are written in lowercase letters and between angle brackets, such as <form> and <input>. Text nodes are inside a box whose title is simply Text.
This representation of an application’s view holds all the information that we need to know to build the DOM. It maintains the hierarchy of the elements, as well as the attributes, the event handlers, and the positions of the child elements. If you are given such a virtual DOM, you can derive the corresponding HTML markup without ambiguity.
Creating virtual trees manually is a tedious task that can result in errors, such as misspelling property names. To simplify the process of defining an application’s view, you write functions that generate each type of virtual node instead of have the developer do the work manually. Although using these functions makes the process of defining the virtual DOM less painful, it’s still not as convenient as writing HTML templates or JSX, but it’s a starting point.
3.4
Types of nodes
49
Exercise 3.1
Given the following HTML markup, can you draw the corresponding virtual DOM tree diagram (similar to figure 3.3)?
<div id="app">
<h1>TODOs</h1>
<input type="text" placeholder="What needs to be done?">
<ul>
<li>
<input type="checkbox">
<label>Buy milk</label>
<button>Remove</button>
</li>
<li>
<input type="checkbox">
<label>Buy eggs</label>
<button>Remove</button>
</li>
</ul>
</div>
Find the solution at
3.3
Getting ready
Make sure that you’ve read appendix A and set up the project structure. All the code you’ll write in this book will be part of the runtime package, which is the part of the framework that runs in the browser. So when I refer to the src/ directory, I mean that of the runtime package until further notice.
You want to create a file called h.js inside the src/ directory. This file is where you’ll write most of the code in this chapter. Also create a utils/ directory inside src/, and add a file called arrays.js inside it. Here, you’ll write a utility function to filter null and undefined values from an array. Your runtime package should look like this (with the configuration files omitted and the files you created in bold):
runtime/
└── src/
├──utils/
│ └── arrays.js
├── h.js
└── index.js
3.4
Types of nodes
Let’s start writing some code. As you’ve seen, you can have three types of DOM nodes that need to be represented as virtual nodes:
50
CHAPTER 3
Rendering and the virtual DOM
Text nodes—They represent text content.
Element nodes—The most common type of node, they represent HTML elements that have a tag name, such as 'div' or 'p'.
Fragment nodes—They represent a collection of nodes that don’t have a parent node until they are attached to the DOM. (I haven’t covered them yet but will in section 3.7.)
These nodes have different properties, so we need to represent them differently. In several chapters of the book, you’ll have to write code that operates on the virtual nodes and do something different depending on the type of node. Therefore, it’s a good idea to define a constant for each type to prevent typos. Inside the h.js file, write the following code:
export const DOM_TYPES = {
TEXT: 'text',
ELEMENT: 'element',
FRAGMENT: 'fragment',
}
You’ve defined three constants, one for each type of node:
DOM_TYPES.TEXT—The type for a text node, which is 'text'
DOM_TYPES.ELEMENT—The type for an element node, which is 'element'
DOM_TYPES.FRAGMENT—The type for a fragment node, which is 'fragment'
Next, we’ll implement the functions that create the virtual nodes, starting with element nodes.
3.5
Element nodes
Element nodes are the most common type of virtual node, representing the regular HTML elements that you use to define the structure of your web pages. To name a few, you have <h1> through <h6> for headings, <p> for paragraphs, <ul> and <ol> for lists, <a> for links, and <div> for generic containers. These nodes have a tag name (such as 'p'), attributes (such as a class name or the type attribute of an <input> element), and children nodes (the nodes that are inside them between the opening and closing tags). In this section, you’ll implement a function h() to create element nodes that take three arguments:
tag—The element’s tag name
props—An object with its attributes (which we’ll call props, for properties)
children—An array of its children nodes
The name h() is short for hyperscript, a script that creates hypertext. (Recall that HTML is the initialism for Hypertext Markup Language.) The name h() for the function is a common one used in some frontend frameworks, probably because it’s short and easy to type, which is important because you’ll be using it often.
3.5
Element nodes
51
h(), hyperscript(), or createElement()
React uses the React.createElement() function to create virtual nodes. The name is a long one, but typically, you never call that function directly; you use JSX instead.
Each HTML element you write in JSX is transpiled to a React.createElement() call.
Other frameworks, such as Vue, name the virtual-node-producing function h().
Mithril, for example, gives the user a function called m() to create the virtual DOM, but the idea is the same. Internally, Mithril implements a virtual-node-creating function named hyperscript(). The user-facing function has a nice short name (m()), but the internal function has a more descriptive name.
The h() function should return a virtual node object with the passed-in tag name, props, and children, plus a type property set to DOM_TYPES.ELEMENT. You want to give default values to the props and children parameters so that you can call the function with only the tag name, as in h('div'), which should be equivalent to calling h('div', {}, []).
Some child nodes might come as null (I’ll explain why soon), so you want to filter them out. To filter null values from an array, in the next section you’ll write a function called withoutNulls(). (That function is imported in listing 3.1 but not implemented yet.)
Some child nodes inside the passed-in children array may be strings, not objects representing virtual nodes. In that case, you want to transform them into virtual nodes of type DOM_TYPES.TEXT, using a function called mapTextNodes() that you’ll write later.
Let’s implement the h() function. In the h.js file, write the code shown in bold in the following listing.
Listing 3.1
The h() function to create element virtual nodes (h.js)
import { withoutNulls } from './utils/arrays'
export const DOM_TYPES = {
TEXT: 'text',
ELEMENT: 'element',
FRAGMENT: 'fragment',
}
export function h(tag, props = {}, children = []) {
return {
tag,
props,
children: mapTextNodes(withoutNulls(children)),
type: DOM_TYPES.ELEMENT,
}
}
52
CHAPTER 3
Rendering and the virtual DOM
3.5.1
Conditional rendering: Removing null values
Now let’s implement the withoutNulls() and mapTextNodes() functions. When we use conditional rendering (rendering nodes only when a condition is met), some children may be null in the array, which means that they shouldn’t be rendered. We want to remove these null values from the array of children.
Let’s use our TODO app from chapter 2 as an example. Recall that the Add to-do button is disabled when the input has no text or the text is too short. If instead of disabling the button, you decided to remove it from the page, you’d have a conditional like the following:
{
tag: 'div',
children: [
{ tag: 'input', props: { type: 'text' } },
addTodoInput.value.length > 2
? { tag: 'button', children: ['Add'] }
: null
]
}
When the condition addTodoInput.value.length > 2 is false, a null node is added to the div node’s children array:
{
tag: 'div',
children: [
{ tag: 'input', props: { type: 'text' } },
null
]
}
This null value means that the button shouldn’t be added to the DOM. The simplest way to make this process work is to filter out null values from the children array when a new virtual node is created so that a null node isn’t passed around the framework:
{
tag: 'div',
children: [
{ tag: 'input', props: { type: 'text' } }
]
}
Inside the utils/arrays.js file, write a function called withoutNulls() that takes an array and returns a new array with all the null values removed:
export function withoutNulls(arr) {
return arr.filter((item) => item != null)
}
3.6
Text nodes
53
Note the use of the != operator, as opposed to !==. You use this operator to remove both null and undefined values. You aren’t expecting undefined values, but this way, you’ll remove them if they appear. Your linter might complain about this approach if you’ve enabled the eqeqeq rule (), but you can disable the rule for this line. Tell the linter you know what you’re doing.
3.5.2
Mapping strings to text nodes
After filtering out the null values from the children array, you pass the result to the mapTextNodes() function. Earlier, I said that this function transforms strings into text virtual nodes. Why do we want to do this? Well, we might do it as a convenience for creating text nodes, so instead of writing
h('div', {}, [hString('Hello '), hString('world!')])
we can write
h('div', {}, ['Hello ', 'world!'])
As you can anticipate, you’ll use text children often, so this function will make your life easier—if only a little. Let’s write that missing mapTextNodes() function now. In the h.js file, below the h() function, write the following code:
function mapTextNodes(children) {
return children.map((child) =>
typeof child === 'string' ? hString(child) : child
)
}
You’ve used the hString() function to create text virtual nodes from strings, but that function doesn’t exist yet. Next, you’ll implement the function that creates text virtual nodes.
3.6
Text nodes
Text nodes are the nodes in the DOM that contain text. They have no tag name, no attributes, and no children—only text.
Text nodes are the simplest of the three types of virtual nodes to create. A text virtual node is simply an object with the type property set to DOM_TYPES.TEXT and the value property set to the text content. In the h.js file, write the hString() function like so:
export function hString(str) {
return { type: DOM_TYPES.TEXT, value: str }
}
That was easy. Now you’re missing only the hFragment() function to create fragment virtual nodes.
54
CHAPTER 3
Rendering and the virtual DOM
3.7
Fragment nodes
A fragment is a type of virtual node used to group multiple nodes that need to be attached to the DOM together but don’t have a parent node in the DOM. You can think of a fragment node as being a container for an array of virtual nodes.
NOTE
Fragments exist in the Document API; they’re used to create subtrees of the DOM that can be appended to the document at the same time. They’re represented by the DocumentFragment class () and can be created by means of the document.createDocumentFragment() method.
We won’t be using the DocumentFragment to insert the virtual fragment nodes into the DOM, but it’s good to know that DocumentFragments exist.
3.7.1
Implementing fragment nodes
In this section, we’ll implement the hFragment() function to create fragment virtual nodes. A fragment is an array of child nodes, so its implementation is simple. In the h.js file, write the hFragment() function as follows:
export function hFragment(vNodes) {
return {
type: DOM_TYPES.FRAGMENT,
children: mapTextNodes(withoutNulls(vNodes)),
}
}
Same as before, you filtered out the null values from the array of children and then mapped the strings in the children array to text virtual nodes. That’s all!
3.7.2
Testing the virtual DOM functions
Now we’ll use the h(), hString(), and hFragment() functions to create virtual DOM representations of the view of your application. We’ll implement the code that takes in a virtual DOM and creates the real DOM for it, but first, we’ll put the virtual DOM functions to the test. Use the h() function to define the view of a login form as follows: h('form', { class: 'login-form', action: 'login' }, [
h('input', { type: 'text', name: 'user' }),
h('input', { type: 'password', name: 'pass' }),
h('button', { on: { click: login } }, ['Log in'])
])
This code creates a virtual DOM, depicted in figure 3.4. Arguably, using the h() functions is more concise than defining the virtual DOM manually as a tree of JavaScript objects.
We’ll use this virtual DOM, passed to the framework, to create the real DOM: the HTML code that will be rendered in the browser. In this case, the HTML markup is
<form class="login-form" action="login">
<input type="text" name="user">
<input type="password" name="pass">
<button>Log in</button>
</form>
3.7
Fragment nodes
55
<form>
class: "login-form"
action: "login"
<input>
<input>
<button>
type: "text"
type: "password"
click: login()
name: "user"
name: "pass"
Text
"Log in"
Figure 3.4
Example of creating a virtual DOM tree
NOTE
The <button> element doesn’t have a click event handler rendered in the HTML markup because the framework will add the event handler to the button programmatically when it’s attached to the DOM. The event handlers added from JavaScript aren’t shown in the HTML.
As you can imagine, you typically don’t define the virtual DOM for your entire application in one place; that process can get unwieldy as the application grows.
Instead, you split the view into parts, each of which we call a component. Components are the cornerstone of frontend frameworks; they allow us to break a large application into smaller, more manageable pieces, each of which is in charge of a specific part of the view.
Exercise 3.2
Using the h() function, define the virtual DOM equivalent to the following HTML
markup:
<h1 class="title">My counter</h1>
<div class="container">
<button>decrement</button>
<span>0</span>
<button>increment</button>
</div>
Find the solution at
56
CHAPTER 3
Rendering and the virtual DOM
Exercise 3.3
Create a function, lipsum(), that takes in a number and returns a virtual DOM consisting of a fragment with as many paragraphs as the number passed to the function. Every paragraph should contain this text: “Lorem ipsum dolor sit amet, consectetur adipisc-ing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea com-modo consequat. ”
This function might come in handy when you’re building a user interface (UI) and need some placeholder text to fill in the space so you can see how the UI looks with real content.
Find the solution
3.8
Components: The cornerstone of frontend frameworks
Let’s see what makes a component in our early version of the framework. The component is the revolutionary concept that has made frontend frameworks popular. The ability to break a large application into smaller parts, each of which defines a specific part of the view and manages its interaction with the user, has been a game-changer (arguably; a good use of the MVC or MVVM pattern could already get us this far).
Every frontend framework uses components, and ours will be no different.
First, we’ll take a small detour from the implementation of the virtual DOM to understand how we’ll decompose the view of our application into a hierarchy of components.
3.8.1
What is a component?
A component in your framework is a mini application of its own. It has its own internal state and lifecycle, and it’s in charge of rendering part of the view. It communicates with the rest of the application, emitting events and receiving props (data passed to the component from the outside), re-rendering its view when a new set of props is passed to it. But we won’t get to that process for a few chapters. Your first version of a component will be much simpler: a pure function that takes in the state of the whole application and returns the virtual DOM representing the view of the component. In chapter 9, you’ll make components that have their own internal state and lifecycle.
For now, start by breaking down the view of the application into pure functions that, given the state, return the virtual DOM representing part of it.
3.8.2
The virtual DOM as a function of the state
The view of an application depends on the state of the application, so we can say that the virtual DOM is a function of the state. Each time the state changes, the virtual DOM
should be reevaluated, and the framework needs to update the real DOM accordingly.
Figure 3.5 depicts this dependency between the state and the view. In the left column, the state consists of a list of to-dos with only one item: “Walk the dog.” In the right
3.8
Components: The cornerstone of frontend frameworks
57
column, the state changes to include a second to-do item: “Water the plants.” Notice that the virtual DOM changes accordingly, and the HTML markup changes as well.
A new to-do is added to the array
of to-dos: the state changes.
state = ["Walk the dog"]
state = ["Walk the dog", "Water the plants" ]
Virtual DOM
Virtual DOM
A new virtual node
<ul>
<ul>
is added.
<li>
<li>
<li>
Text
Text
Text
"Walk the dog"
"Walk the dog"
"Water the plants"
HTML
HTML
A new HTML element
is added.
<ul>
<ul>
<li>Walk the dog</li>
<li>Walk the dog</li>
<li>Water the plants</li>
</ul>
</ul>
Figure 3.5
The view of an application is a function of the state. When a new to-do item is added to the state, the virtual DOM is reevaluated, and the DOM is updated with the new to-do.
To produce the virtual DOM representing the view, we must take the current state of the application into account, and when the state changes, we must reevaluate the virtual DOM. If we generate the virtual DOM that represents the view of the application by calling a function that receives the state as a parameter, we can easily reevaluate it when the state changes. In the case of our TODOs application, the virtual DOM for the list of to-dos (consisting of only the to-do description for the sake of simplicity) could be generated by a function like this:
function TodosList(todos) {
return h('ul', {}, todos.map((todo) => h('li', {}, [todo])))
}
If we call the TodosList() function with the following list of to-dos as an argument, TodosList(['Walk the dog', 'Water the plants'])
the function would return the following virtual DOM (empty props objects omitted for brevity):



58
CHAPTER 3
Rendering and the virtual DOM
{
tag: 'ul',
type: 'element',
children: [
{ tag: 'li', children: [{ type: 'text', value: 'Walk the dog' }] },
{ tag: 'li', children: [{ type: 'text', value: 'Water the plants' }] }
]
}
The framework would render the virtual DOM representing the list of to-dos into the following HTML:
<ul>
<li>Walk the dog</li>
<li>Water the plants</li>
</ul>
Figure 3.6 summarizes this process. The application code, written by the developer, generates the virtual DOM describing the view. Then the framework, which you wrote, creates the real DOM from the virtual DOM and inserts it into the browser’s document.
js
js
Application
Framework
code
code
DOM
Virtual DOM
The framework code takes
The application code
a virtual DOM and generates
defines how its view
the corresponding DOM
looks by using a
elements.
virtual DOM.
The framework inserts
the DOM elements into
the browser’s document.
Figure 3.6
The application creates the virtual DOM that the framework renders into the DOM.
3.8
Components: The cornerstone of frontend frameworks
59
One nice benefit of using functions to produce the virtual DOM, receiving the application’s state as an argument, is that we can break the view into smaller parts. We can easily compose pure functions—functions that don’t produce any side effects and always return the same result for the same input arguments—to build more complex views.
Exercise 3.4
Write a function called MessageComponent() that takes an object with two properties:
level—A string that can be 'info', 'warning', or 'error'
message—A string with the message to display
The function should return a virtual DOM that represents a message box with the message and the corresponding CSS class depending on the level. The CSS classes are message--info, message--warning, and message--error for the different levels. The markup should look like this:
<div class="message message--info">
<p>This is an info message</p>
</div>
Find the solution at .
3.8.3
Composing views: Components as children
Pure functions can be composed nicely to build more complex functions. If we generate the virtual DOM to represent the view of the application by composing smaller functions, we can easily break the view into smaller parts. These subfunctions represent part of the view: the components. The arguments passed to a component are known as props, as I’ve already mentioned.
NOTE
In this first version of the framework, components are functions that generate the virtual DOM for part of the application’s view. They take the state of the application or part of it as their argument. The arguments passed to a component, the data coming from outside the component, are known as props.
This definition of a component changes in chapter 9, where components are more than pure functions and handle their own state and lifecycle.
Let’s work on an example that uses the TODOs application view. If we don’t break the view into smaller parts, we’ll have a single function that generates the virtual DOM for the entire application. Such a function would be long and hard for other developers to understand—and probably hard for us to understand, too. But we can clearly distinguish two parts in the view: the form to add a new to-do item and the list of to-dos (figure 3.7), which be generated by two different functions. That is, we can break the view into two subcomponents.
60
CHAPTER 3
Rendering and the virtual DOM
My TODOs
New TODO
CreateTodo()
Add
- Walk the dog
Done
Figure 3.7
The TODOs
-
Water the plants
Save
Cancel
TodoList()
application can be broken
into two subcomponents:
CreateTodo() and
- Sand the chairs
Done
TodoList().
So the virtual DOM for the whole application could be created by a component like the following:
function App(state) {
return hFragment([
h('h1', {}, ['My TODOs']),
CreateTodo(state),
TodoList(state)
])
}
Note that in this case, no parent node in the virtual DOM contains the header of the application and the two subcomponents; we use a fragment to group the elements.
Also note the naming convention: the functions that generate the virtual DOM are written in PascalCase to signal that they’re components that create a virtual DOM
tree, not regular functions.
Similarly, the TodoList() component, as you’ve probably guessed, can be broken down into another subcomponent: the TodoItem(). You can see this subcomponent in figure 3.8.
- Walk the dog
Done
TodoItem()
-
Water the plants
Save
Cancel
TodoItem()
TodoList()
- Sand the chairs
Done
TodoItem()
Figure 3.8
The TodoList() component can be broken down into a TodoItem() subcomponent.
Thus, the TodoList() component would look similar to the following:
3.8
Components: The cornerstone of frontend frameworks
61
function TodoList(state) {
return h('ul', {},
children: state.todos.map(
(todo, i) => TodoItem(todo, i, state.editingIdxs)
)
)
}
The TodoItem() component would render a different thing depending on whether the to-do is in read or edit mode. Those components could be further decomposed into two different subcomponents: TodoInReadMode() and TodoInEditMode(). This split would be something like the following:
// idxInList is the index of this todo item in the list of todos.
// editingIdxs is a Set of indexes of todos that are being edited.
function TodoItem(todo, idxInList, editingIdxs) {
const isEditing = editingIdxs.has(idxInList)
return h('li', {}, [
isEditing
? TodoInEditMode(todo, idxInList)
: TodoInReadMode(todo, idxInList)
]
)
}
Defining the views of our application by using pure functions—the components—
allows you to compose them easily to build more complex views. This process probably isn’t new to you; you’ve been decomposing your applications into a hierarchy of components when using frontend frameworks like React, Vue, Svelte, or Angular. We can visualize the hierarchy of components for the preceding example in a tree, as shown in figure 3.9.
In this figure, we see the view of the application as a tree of components with the virtual DOM nodes they generate below them. The App() component is the root of the tree, and it has three children: an <h1> element, the CreateTodo() component, and the TodoList() component. A component can return only a single virtual DOM
node as its root, so the three children of App() are grouped in a fragment.
Then, following the hierarchy down, we see that the TodoList() component has a single child, the <ul> element, which in turn has a list of TodoItem() components.
The ellipsis in the tree indicates that the <ul> element may have more children than the ones shown in the figure—a number that depends on how many to-dos are in the list. Finally, the TodoItem() component has two children: the TodoInEditMode() and TodoInReadMode() components. These components would render more virtual DOM
nodes, but I don’t show them in the figure for simplicity.
NOTE
As you can see in figure 3.9, the component nodes have their titles written in PascalCase and include parentheses to indicate that they’re functions. Each fragment is titled Fragment.
62
CHAPTER 3
Rendering and the virtual DOM
App()
If a component returns more
props:
than a parent node, it needs
- state
to wrap it inside a fragment.
Fragment
<h1>
CreateTodo()
TodoList()
props:
props:
- state
- state
Text
<ul>
A component receives data
"My TODOs"
from the outside as props.
list
TodoItem()
...
props:
- todo
- idxInList
Dynamic list of
- editingIdxs
child components
A different component is rendered
<ul>
depending on whether the to-do is
being edited.
Yes
Is
No
edited?
TodoInEditMode()
TodoInReadMode()
props:
props:
- todo
- todo
- idxInList
- idxInList
...
...
Figure 3.9
The hierarchy of components of the TODOs application, in which each component has its child virtual DOM nodes below itself
Now that you understand how to break the view of your application into components—pure functions that generate the virtual DOM given the state of the application—you’re ready to implement the code in the framework, which is in charge of mounting the virtual DOM returned by the h() functions to the browser’s DOM.
You’ll do exactly that in chapter 4.
Summary
63
Summary
The virtual DOM is the blueprint for the view of the application. It allows the developer to describe how the view should look in a declarative way (similar to what an architect does with the blueprints of a house) and moves responsibility for manipulating the DOM to the framework.
By being able to use a virtual DOM to declare what the view should look like, the developer doesn’t have to know how to manipulate the DOM by using the Document API and doesn’t need to mix business logic with DOM-manipulation code.
A component is a pure function—a function with no side effects—that takes the state of the application as input and returns a virtual DOM tree representing a chunk of the view of the application. In later chapters, the definition of a component will be extended to include the ability to have internal state and a lifecycle that’s independent of the application’s.
The three types of virtual nodes are text, element, and fragment. The most interesting one is the element node, which represents the regular HTML elements that can have attributes, children, and event listeners.
The hString(), h(), and hFragment() functions create text, element, and fragment virtual nodes, respectively. The virtual DOM can be declared directly as a tree of JavaScript objects, but calling these functions makes the process simpler.
Fragment virtual nodes consist of an array of children virtual nodes.
Fragment virtual nodes are useful when a component returns a list of virtual nodes without a parent node. The DOM—and, by extension, the virtual DOM—
is a tree data structure. Every level of the tree (except the root) must have a parent node, so a fragment node can be used to group a list of virtual nodes.
Mounting and destroying
the virtual DOM
This chapter covers
Creating HTML nodes from virtual DOM nodes
Inserting HTML nodes into the browser’s
document
Removing HTML nodes from the browser’s
document
In chapter 3, you learned what the virtual Document Object Model (DOM) is and how to create it. You implemented the h(), hString(), and hFragment() functions to create virtual nodes of type element, text, and fragment, respectively. Now it’s time to learn how to create the real DOM nodes from the virtual DOM nodes and insert them into the browser’s document. You achieve this task by using the Document API, as you’ll see in this chapter.
When the view of your application is no longer needed, you want to remove the HTML nodes from the browser’s document. You’ll learn how in this chapter as well.
NOTE
You can find all the listings in this chapter in the listings/ch04
directory of the book’s repository The code you write in this chapter is for the framework’s first version, which you’ll publish 64
4.1
Mounting the virtual DOM
65
in chapter 6. Therefore, the code in this chapter can be checked out from the ch6 label ( $ git switch --detach ch6.
Code catch-up
In chapter 3, you implemented the h(), hString(), and hFragment() functions.
These functions are used to create virtual nodes of type 'element', 'text', and
'fragment', respectively. You also defined the DOM_TYPES object containing the different types of virtual nodes.
4.1
Mounting the virtual DOM
Given a virtual DOM tree, you want your framework to create the real DOM tree from it and attach it to the browser’s document. We call this process mounting the virtual DOM. You implement this code in the framework so that the developers who use it don’t need to use the Document API themselves. You’ll implement this process in the mountDOM() function.
Figure 4.1 is a visual representation of how the mountDOM() function works. You can see that the first argument is a virtual DOM, and the second argument is the parent element where we want the view to be inserted: the document’s <body> element.
The result is a DOM tree attached to the parent element, which is the <body> of the document.
mountDOM( ,
<body>
)
</body>
The virtual DOM is
The parent element to
passed to the function.
which the DOM is attached
<body>
The DOM elements are
attached to the passed-in
parent element (in this
case, the <body>).
</body>
Figure 4.1
Mounting a virtual DOM to the browser’s document <body> element
66
CHAPTER 4
Mounting and destroying the virtual DOM
WARNING
In many examples in this book and the tests in the repository, I mounted the application inside the <body> element of the document. This approach is okay for the examples and tests, but you should avoid it in your applications. Some third-party libraries might attach elements to the <body> element, which might make your reconciliation algorithm work incorrectly.
When the mountDOM() function creates each DOM node for the virtual DOM, it needs to save a reference to the real DOM node in the virtual node under the el property (el for element), as you see in figure 4.2. The reconciliation algorithm that you’ll write in chapters 7 and 8 uses this reference to know what DOM nodes to update.
<input>
Mount element.
<input
type="text"
type: "text"
el
name="user"
Save a reference to
name: "user"
/>
the element.
Figure 4.2
The virtual node’s el property keeps a reference to the real DOM node.
Similarly, if the node includes event listeners, the mountDOM() function saves a reference to the event listener in the virtual node under the listeners property (figure 4.3).
Mount element.
<button>
<button>
...
el
</button>
click: login()
Save a reference to
the element.
Save a reference to
the event listener.
Figure 4.3
The virtual node’s listeners property keeps a reference to
the event listener.
Saving these references has a double purpose: it allows the framework to remove the event listeners and detach the element from the DOM when the virtual node is unmounted, and the reconciliation algorithm requires it to know what element in the DOM needs to be updated. This process will become clear in chapter 7; for now, bear with me. Using the example from earlier, the virtual DOM we defined as const vdom = h('form', { class: 'login-form', action: 'login' }, [
h('input', { type: 'text', name: 'user' }),
h('input', { type: 'password', name: 'pass' }),
h('button', { on: { click: login } }, ['Login'])
])
4.1
Mounting the virtual DOM
67
passed to the mountDOM() function as
mountDOM(vdom, document.body)
would result in the virtual DOM tree depicted in figure 4.4, where you can see the el and listeners references in the virtual nodes.
<form>
The el and listeners
references are added
class: "login-form"
el
to the virtual nodes.
action: "login"
<input>
<input>
<button>
el
type: "text"
type: "password"
click: login()
el
el
name: "user"
name: "pass"
Text
"Login"
el
Figure 4.4
The login form virtual DOM example mounted to the browser’s document <body> element. The virtual nodes keep a reference to the real DOM nodes in the el property and to the event listeners in the listeners property (shown as a lightning-bolt icon).
This HTML tree would be attached to the <body> element, and the resulting HTML
markup would be
<body>
<form class="login-form" action="login">
<input type="text" name="user">
<input type="password" name="pass">
<button>Login</button>
</form>
</body>
Different types of virtual nodes require different DOM nodes to be created:
A virtual node of type text requires a Text node to be created (via the document
.createTextNode() method).
A virtual node of type element requires an Element node to be created (via the document.createElement() method).
The mountDOM() function needs to differentiate between the values of the type property of the virtual node and create the appropriate DOM node. With this fact in mind, let’s implement the mountDOM() function.
68
CHAPTER 4
Mounting and destroying the virtual DOM
4.1.1
Mounting virtual nodes into the DOM
Create a new file called mount-dom.js in the src/ directory. Then write the mountDOM() function as shown in the following listing. The listing includes some TODO comments toward the end of the file. You don’t need to write those comments in your code; they’re placeholders to show where you will implement the missing functions later.
Listing 4.1
The mountDOM() function mounting the virtual DOM (mount-dom.js) import { DOM_TYPES } from './h'
export function mountDOM(vdom, parentEl) {
switch (vdom.type) {
case DOM_TYPES.TEXT: {
Mounts a text
virtual node
createTextNode(vdom, parentEl)
break
}
case DOM_TYPES.ELEMENT: {
Mounts an element
virtual node
createElementNode(vdom, parentEl)
break
}
Mounts the children
of a fragment virtual
case DOM_TYPES.FRAGMENT: {
node
createFragmentNodes(vdom, parentEl)
break
}
default: {
throw new Error(`Can't mount DOM of type: ${vdom.type}`)
}
}
}
// TODO: implement createTextNode()
// TODO: implement createElementNode()
// TODO: implement createFragmentNodes()
The function uses a switch statement that checks the type of the virtual node.
Depending on the node’s type, the appropriate function to create the real DOM node gets called. If the node type isn’t one of the three supported types, the function throws an error. If you made a mistake, such as misspelling the type of a virtual node, this error will help you find it.
4.1.2
Mounting text nodes
Text nodes are the simplest type of node to create because they don’t have any attributes or event listeners. To create a text node, the Document API provides the createTextNode() method This method expects a string as
4.1
Mounting the virtual DOM
69
an argument, which is the text that the text node will contain. If you recall, the virtual nodes created by the hString() function you implemented earlier have the following structure:
{
type: DOM_TYPES.TEXT,
value: 'I need more coffee'
}
These virtual nodes have a type property, identifying them as a text node, and a value property, which is set to the string that the hString() function receives as an argument. You pass this text to the createTextNode() method. After creating the text DOM node, you have to do two things:
1
Save a reference to the real DOM node in the virtual node under the el property.
2
Attach the text node to the parent element.
Inside the mount-dom.js file, write the createTextNode() function as follows: function createTextNode(vdom, parentEl) {
const { value } = vdom
Creates a
text node
const textNode = document.createTextNode(value)
vdom.el = textNode
Saves a reference
of the node
parentEl.append(textNode)
Appends to the
}
parent element
Exercise 4.1
Using the hString() and mountDOM() functions, insert the text OMG, so interesting!
below the headline of your local newspaper’s website. (You’ll need to copy/paste some of your code in the browser’s console.)
Find the solution at
4.1.3
Mounting fragment nodes
Let’s implement the createFragmentNodes() function. Fragment nodes are simple to mount; you simply mount the children of the fragment. It’s important to remember that fragments aren’t nodes that get attached to the DOM; they’re an array of children. For this reason, the el property of a fragment virtual node should point to the parent element to which the fragment’s children are attached (figure 4.5).
Note that if you have nested fragment nodes, all the fragment nodes’ children will be appended to the same parent element. All the el references of those fragment virtual nodes should point to the same parent element, as shown in figure 4.6.
70
CHAPTER 4
Mounting and destroying the virtual DOM
The el referenced in the
virtual node should be the
parent where the DOM is
The <body> is where
Fragment
mounted, not the fragment.
the fragment children
el
are mounted.
<h1>
<p>
<body>
el
el
<h1>My Blog</h1>
mountDOM()
<p>Welcome!</p>
</body>
Text
Text
el
el
"My Blog"
"Welcome!"
Figure 4.5
A fragment’s el should reference the parent element to which its children are attached.
Fragment
el
Fragment
el
These three references all
point to the <body> element.
Fragment
el
<h1>
<p>
<body>
el
el
<h1>My Blog</h1>
mountDOM()
<p>Welcome!</p>
</body>
Text
Text
el
el
"My Blog"
"Welcome!"
Figure 4.6
All nested fragments point to the same parent element.
Now that you have a good understanding of how fragments work, it’s time to write some code. Write the createFragmentNode() function inside the mount-dom.js file as follows:
function createFragmentNodes(vdom, parentEl) {
const { children } = vdom
Saves a reference of
the parent element
vdom.el = parentEl
children.forEach((child) => mountDOM(child, parentEl))
Appends each
}
child to the
parent element
4.1
Mounting the virtual DOM
71
Great! Now you’re ready to implement the createElementNode() function. This function is the most important one because it creates the element nodes—the visual bits of the DOM tree.
4.1.4
Mounting element nodes
To create element nodes (those regular HTML elements with tags like <div> and
<span>), you use the createElement() method from the Document API
You have to pass the tag name to the createElement() function. The Document API returns an element node that matches that tag or HTMLUnknownElement (if the tag is unrecognized. You can try this code yourself in the browser console:
Object.getPrototypeOf(document.createElement('foobar')) // HTMLUnknownElement An obviously nonexistent tag such as foobar returns an HTMLUnknownElement node.
So if a virtual node has a tag that the Document API doesn’t recognize, the document
.createElement() function returns an HTMLUnknownElement node. We’re not going to worry about this case: if an HTMLUnknownElement results from the createElement() function, we’ll assume an error on the developer’s part.
If you recall, calling the h() function to create element virtual nodes returns an object with the type property set to DOM_TYPES.ELEMENT, a tag property with the tag name, and a props property with the attributes and event listeners. If the virtual node has children, they appear inside the children property. A <button> virtual node with a class of btn and an onclick event listener would look like this:
{
type: DOM_TYPES.ELEMENT,
tag: 'button',
props: {
class: 'btn',
on: { click: () => console.log('yay!') }
},
children: [
{
type: DOM_TYPES.TEXT,
value: 'Click me!'
}
]
}
To create the corresponding DOM element from the virtual node, you need to do the following:
1
Create the element node using the document.createElement() function.
2
Add the attributes and event listeners to the element node, saving the added event listeners in a new property of the virtual node called listeners.
3
Save a reference to the element node in the virtual node under the el property.
72
CHAPTER 4
Mounting and destroying the virtual DOM
4
Mount the children recursively into the element node.
5
Append the element node to the parent element.
As you can see, the props property of the virtual node contains the attributes and event listeners. But attributes and event listeners are handled differently, so you’ll need to separate them.
Two special cases that are related to styling also need special handling: style and class. You’ll extract them from the props object and handle them separately. Write the createElementNode() function inside the mount-dom.js file, as shown in the following listing.
Listing 4.2
Mounting an element node into a parent element (mount-dom.js)
import { setAttributes } from './attributes'
import { addEventListeners } from './events'
// --snip-- //
function createElementNode(vdom, parentEl) {
const { tag, props, children } = vdom
Creates an
element node
const element = document.createElement(tag)
addProps(element, props, vdom)
Adds the
vdom.el = element
attributes and
event listeners
children.forEach((child) => mountDOM(child, element))
parentEl.append(element)
}
function addProps(el, props, vdom) {
Splits listeners
from attributes
const { on: events, ...attrs } = props
vdom.listeners = addEventListeners(events, el)
Adds event
setAttributes(el, attrs)
listeners
Sets attributes
}
Note that you’re using two functions you haven’t implemented yet: setAttributes() and addEventListeners(), imported from the attributes.js and events.js files, respectively. You’ll write them soon.
Setting the attributes and adding event listeners is the part where the code differs from text nodes. With text nodes, you want to attach event listeners and set attributes to the parent element node, not to the text node itself.
NOTE
The Text type defined in the Document API inherits from the Event-
Target interface which declares the addEventListener() method. In principle, you can add event listeners to a text node, but if you try to do that in the browser, you’ll see that the event listeners are never called.
4.1
Mounting the virtual DOM
73
Next, we’ll implement the addEventListeners() function, which is in charge of adding event listeners to an element node. Then we’ll look at the setAttributes() function, and we’ll be done with creating element nodes.
4.1.5
Adding event listeners
To add an event listener to an element node, you call its addEventListener() method (This method is available because an element node is an instance of the EventTarget interface. This interface, which declares the addEventListener() method, is implemented by all the DOM nodes that can receive events.
All instances returned by calling document.createElement() implement the EventTarget interface, so you can safely call the addEventListener() method on them.
Our implementation of the addEventListener() function in this chapter will be very simple: it will call the addEventListener() method on the element and return the event handler function it registered. You want to return the function registered as an event handler because later, when you implement the destroyDOM() method (as you can figure out, it does the opposite of mountDOM()), you’ll need to remove the event listeners to avoid memory leaks. You need the handler function that was registered in the event listener to be able to remove it by passing it as an argument to the removeEventListener() method.
In chapter 9, when you make the components of your framework stateful (they’ll be pure functions with no state for the moment), you’ll also have references to the event listeners added to the components, which will behave differently from the event handlers of the DOM nodes. At that point in the book, you’ll need to modify the implementation of the addEventListener() function to account for this new case. So if you wonder why we’re implementing a function that does so little on its own, the reason is that it’s a placeholder for the more complex implementation that you’ll write later. Create a new file under the src/ directory called events.js, and add the following code:
export function addEventListener(eventName, handler, el) {
el.addEventListener(eventName, handler)
return handler
}
As promised, the addEventListener() function is simple. But if you recall, the event listeners defined in a virtual node come packed in an object. The keys are the event names, and the values are the event handler functions, like so:
{
type: DOM_TYPES.ELEMENT,
tag: 'button',
props: {
on: {
mouseover: () => console.log('almost yay!'),
click: () => console.log('yay!') ,
74
CHAPTER 4
Mounting and destroying the virtual DOM
dblclick: () => console.log('double yay!'),
}
}
}
It makes sense to have another function, if only for convenience, that allows you to add multiple event listeners in the form of an object to an element node. Inside the events.js file, add another function called addEventListeners() (plural): export function addEventListeners(listeners = {}, el) {
const addedListeners = {}
Object.entries(listeners).forEach(([eventName, handler]) => {
const listener = addEventListener(eventName, handler, el)
addedListeners[eventName] = listener
})
return addedListeners
}
You may be tempted to simplify that function by removing the addedListeners variable and return the same listeners object that the function got as input: export function addEventListeners(listeners = {}, el) {
Object.entries(listeners).forEach( ... )
return listeners
}
After all, the same event handler functions that we got as input, we’re returning as output, so the refactor seems to be legit. But even though it may be legitimate now, that won’t be the case later, when the components have their own state. You won’t be adding the same functions as event handlers, but new functions created by the framework with some extra logic around them. This process might sound confusing, but stick with me, and you’ll see how it makes sense.
Now that you’ve implemented the event listeners, let’s implement the setAttributes() function. We’re getting closer to having a working mountDOM() function.
4.1.6
Setting the attributes
To set an attribute in an HTMLElement instance in code, you set the value in the corresponding property of the element. Setting the property of the element reflects the value in the corresponding attribute of the rendered HTML. It’s important to understand that when you’re manipulating the DOM through code, you’re working with DOM nodes—instances of the HTMLElement class. These instances have properties that you can set in code, as with any other JavaScript object. When these properties are set, the corresponding attribute is automatically reflected in the rendered HTML.
If you have a paragraph in HTML, such as,
<p id="foo">Hello, world!</p>
4.1
Mounting the virtual DOM
75
and assuming that you have a reference to the <p> element in a variable called p, you can set the id property of the p element to a different value, like so: p.id = 'bar'
The rendered HTML reflects the change:
<p id="bar">Hello, world!</p>
In a nutshell, HTMLElement instances (such as the <p> element, which is an instance of the HTMLParagraphElement class) have properties that correspond to the attributes that are rendered in the HTML markup. When you set the value of these properties, the corresponding attributes in the rendered HTML are updated automatically. Even though the process is a bit more nuanced, this discussion gives you the gist.
Caution on attributes
Some attributes work a bit differently. The value attribute of an <input> element, for example, isn’t reflected in the rendered HTML. Suppose that you have the HTML
<input type="text" />
and programmatically set its value like so:
input.value = 'yolo'
You’ll see the string "yolo" in the input, but the rendered HTML will be the same; no value attribute will be rendered. Even more interesting is the fact that you can add the attribute in the HTML markup:
<input type="text" value="yolo" />
The "yolo" string will appear in the input when it first renders, but if you type something different, the same value for the value attribute remains in the rendered HTML.
You can read whatever was typed in the input by reading the value property of the input element. You can read more about this behavior in the HTML specification at
.
Nevertheless, we’ll handle two special attributes differently: style and class. We’ll start by writing the setAttributes() function: the one used in listing 4.2 to set the attributes on the element node. The role of this function is to extract the attributes that require special handling (style and class) from the rest of the attributes and then call the setStyle() and setClass() functions to set those attributes. The rest of the attributes are passed to the setAttribute() function. You’ll write the setStyle(), setClass(), and setAttribute() functions later. Start by creating the attributes.js file in the src/ directory and entering the code in the following listing.
76
CHAPTER 4
Mounting and destroying the virtual DOM
Listing 4.3
Setting the attributes of an element node (attributes.js)
export function setAttributes(el, attrs) {
const { class: className, style, ...otherAttrs } = attrs
Splits the
attributes
if (className) {
setClass(el, className)
Sets the class attribute
}
if (style) {
Object.entries(style).forEach(([prop, value]) => {
setStyle(el, prop, value)
Sets the style
})
attribute
}
for (const [name, value] of Object.entries(otherAttrs)) {
setAttribute(el, name, value)
Sets the rest of
}
the attributes
}
// TODO: implement setClass
// TODO: implement setStyle
// TODO: implement setAttribute
Now you have the function that splits the attributes into ones that require special handling and the rest and then calls the appropriate functions to set them. The following sections discuss those functions separately.
SETTING THE CLASS ATTRIBUTE
The setClass() function is in charge of setting the class attribute. Note that you’ve destructured the attrs property and aliased the class attribute to the className variable, as class is a reserved word in JavaScript. When you write HTML, you set the class attribute of an element node like this:
<div class="foo bar baz"></div>
In this case, the <div> element has three classes: foo, bar, and baz. Easy! But now comes the tricky part: a DOM element (an instance of the Element class;
doesn’t have a class property. Instead, it has two properties, className
and classList ), that are related to the class attribute.
The classList property returns an object—a DOMTokenList
, to be specific—that comes in handy when you want to add, remove, or toggle classes on an element. A DOMTokenList object has an add() method (
that takes multiple class names and adds them to the element. If you had a
<div> element like
<div></div>
4.1
Mounting the virtual DOM
77
and wanted to add the foo, bar, and baz classes to it, you could do it this way: div.classList.add('foo', 'bar', 'baz')
This code would result in the following HTML:
<div class="foo bar baz"></div>
Next is the className property, a string that contains the value of the class attribute. Following the preceding example, if you want to add the same three classes to the <div> element, you could do it this way and get the same HTML: div.className = 'foo bar baz'
You may want to set the class attribute both ways, depending on the situation. You should allow the developers who use your framework to set the class attribute either as a string or as an array of string items. So to add multiple classes to an element, a developer could define the following virtual node:
{
type: DOM_TYPES.ELEMENT,
tag: 'div',
props: {
class: ['foo', 'bar', 'baz']
}
}
Alternatively, they could use a single string:
{
type: DOM_TYPES.ELEMENT,
tag: 'div',
props: {
class: 'foo bar baz'
}
}
Both of these options should work. Thus, the setClass() function needs to distinguish between the two cases and handle them accordingly. With this fact in mind, write the following code in the attributes.js file:
function setClass(el, className) {
el.className = ''
Clears the class attribute
if (typeof className === 'string') {
el.className = className
Class attribute as a string
}
if (Array.isArray(className)) {
el.classList.add(...className)
Class attribute as an array
}
}
78
CHAPTER 4
Mounting and destroying the virtual DOM
SETTING THE STYLE ATTRIBUTE
With the setClass() function out of the way, let’s move to the setStyle() function, which is in charge of setting the style attribute of an element. The style property
of an HTMLElement instance is a CSSStyleDeclaration object
. You can set the value of a CSS property by using conventional object notation, like this:
element.style.color = 'red'
element.style.fontFamily = 'Georgia'
Changing the style property key-value pairs of an HTMLElement instance is reflected in the value of the style attribute of the element. If the element in the preceding snippet were a paragraph (<p>), the resulting HTML would be
<p style="color: red; font-family: Georgia;"></p> The CSSStyleDeclaration is converted to a string with a set of semicolon-separated key-value pairs. You can inspect this string representation of the style attribute by using the cssText property in the code: element.style.cssText // 'color: red; font-family: Georgia;'
Using the element from the preceding snippet, you could remove the color style as follows:
element.style.color = null
element.style.cssText // 'font-family: Georgia;'
Now that you know how to work with the style property of an HTMLElement instance, write the setStyle() and removeStyle() functions. The first function takes an HTMLElement instance, the name of the style to set, and the value of the style, and it sets that style on the element. The second function takes an HTMLElement instance and the name of the style to remove, and it removes that style from the element.
Inside the attributes.js file, write the following code:
export function setStyle(el, name, value) {
el.style[name] = value
}
export function removeStyle(el, name) {
el.style[name] = null
}
Note that you haven’t used the removeStyle() function yet. The code you wrote before used only the setStyle() function. But because you’ll need to remove styles later, now is a good time to write it.
You’re almost done. You’re missing only the setAttributes() function, which is in charge of setting the attributes other than class and style.
4.1
Mounting the virtual DOM
79
SETTING THE REST OF THE ATTRIBUTES
The setAttribute() function takes three arguments: an HTMLElement instance, the name of the attribute to set, and the value of the attribute. If the value of the attribute is null, the attribute is removed from the element. (Conventionally, setting a DOM
element’s property to null is the same as removing the attribute.) If the attribute is of the form data-*, the attribute is set using the setAttribute() function. Otherwise, the attribute is set to the given value using object notation (object.key = value). (In other words, the property of the DOM element is set to the given value, and the attribute in the HTML reflects that value.)
To remove an attribute, you want to both set it to null and remove it from the attributes object, using the removeAttribute() method. Inside the attributes.js file, write the following code:
export function setAttribute(el, name, value) {
if (value == null) {
removeAttribute(el, name)
} else if (name.startsWith('data-')) {
el.setAttribute(name, value)
} else {
el[name] = value
}
}
export function removeAttribute(el, name) {
el[name] = null
el.removeAttribute(name)
}
With this last function, you’re done implementing the mountDOM() function, which takes a virtual DOM and mounts it to the real DOM inside the passed-in parent element. Congratulations!
4.1.7
A mountDOM() example
Thanks to this function, you can define a view by using the virtual DOM representation of it and mount it to the real DOM. If you create a view like this, const vdom = h('section', {} [
h('h1', {}, ['My Blog']),
h('p', {}, ['Welcome to my blog!'])
])
mountDOM(vdom, document.body)
the resulting HTML would be
<body>
<section>
<h1>My Blog</h1>
<p>Welcome to my blog!</p>
80
CHAPTER 4
Mounting and destroying the virtual DOM
</section>
</body>
Now you want a function that, given a mounted virtual DOM, destroys it and removes it from the document. With this function, destroyDOM(), you’ll be able to clear the document’s body from the preceding example:
destroyDOM(vdom, document.body)
The work done by mountDOM() is undone by destroyDOM(). This code would make the document’s body empty again:
<body></body>
Exercise 4.2
Using the hFragment(), h(), and mountDOM() functions, insert a new section below the headline of your local newspaper’s website. The section should have a title, a paragraph, and a link to an article in Wikipedia. Be creative!
Find the solution .
Exercise 4.3
Following up on exercise 4.2, inspect the virtual DOM tree in the browser’s console.
Check the el property of the fragment virtual node. What does it point to? What about the el property of the paragraph and link virtual nodes?
Find the solution .
4.2
Destroying the DOM
Let’s close this chapter by implementing the destroyDOM() function. Destroying the DOM is simpler than mounting it. Well, destroying anything is always simpler than creating it in the first place. Destroying the DOM is the process in which the HTML
elements that the mountDOM() function created are removed from the document (figure 4.7).
To destroy the DOM associated with a virtual node, you have to take into account what type of node it is:
Text node—Remove the text node from its parent element, using the remove() method.
Fragment node—Remove each of its children from the parent element (which, if you recall, is referenced in the el property of the fragment virtual node).
Element node—Do the two preceding things and remove the event listeners from the element.
4.2
Destroying the DOM
81
Mounted virtual DOM
passed to the function
destroyDOM( )
Mounted DOM before
being destroyed
Destroyed DOM
<body>
<body>
</body>
</body>
Figure 4.7
Destroying the DOM
In all cases, you want to remove the el property from the virtual node, and in the case of an element node, you also remove the listeners property so you can tell that the virtual node has been destroyed, allowing the garbage collector to free the memory of the HTML element. When a virtual node doesn’t have an el property, you can safely assume that it’s not mounted to the real DOM and therefore can’t be destroyed. To handle these three cases, you need a switch statement that (depending on the type property of the virtual node) calls a different function.
You’re ready to implement the destroyDOM() function. Create a new file inside the src/ directory called destroy-dom.js, and enter the code shown in the following listing.
Listing 4.4
Destroying the virtual DOM (destroy-dom.js)
import { removeEventListeners } from './events'
import { DOM_TYPES } from './h'
export function destroyDOM(vdom) {
const { type } = vdom
switch (type) {
case DOM_TYPES.TEXT: {
removeTextNode(vdom)
break
}
case DOM_TYPES.ELEMENT: {
removeElementNode(vdom)
break
}
82
CHAPTER 4
Mounting and destroying the virtual DOM
case DOM_TYPES.FRAGMENT: {
removeFragmentNodes(vdom)
break
}
default: {
throw new Error(`Can't destroy DOM of type: ${type}`)
}
}
delete vdom.el
}
// TODO: implement removeTextNode()
// TODO: implement removeElementNode()
// TODO: implement removeFragmentNodes()
You’ve written the algorithm for destroying the DOM associated with a passed-in virtual node: vdom. You’ve handled each type of virtual node separately; you’ll need to write the missing functions in a minute. Finally, you’ve deleted the el property from the virtual node. Figure 4.8 depicts this process.
Fragment
The el references are
The content of <body>
deleted from the nodes.
is removed from the DOM.
el
<h1>
<p>
<body>
el
el
<h1>My Blog</h1>
destroyDOM()
<p>Welcome!</p>
</body>
Text
Text
el
el
"My Blog"
"Welcome!"
Figure 4.8
Removing the el references from the virtual nodes
You’ve imported the removeEventListeners() function from the events.js file, but you haven’t implemented that one yet. You will soon. Let’s start with the code for destroying a text node.
4.2.1
Destroying a text node
Destroying a text node is the simplest case:
function removeTextNode(vdom) {
const { el } = vdom
el.remove()
}
4.2
Destroying the DOM
83
4.2.2
Destroying an element
The code for destroying an element is a bit more interesting. To destroy an element, start by removing it from the DOM, similar to what you did with a text node. Then recursively destroy the children of the element by calling the destroyDOM() function for each of them. Finally, remove the event listeners from the element and delete the listeners property from the virtual node. First, implement the removeElementNode() function in the destroy-dom.js file:
function removeElementNode(vdom) {
const { el, children, listeners } = vdom
el.remove()
children.forEach(destroyDOM)
if (listeners) {
removeEventListeners(listeners, el)
delete vdom.listeners
}
}
Here’s where you used the missing removeEventListeners() function to remove the event listeners from the element. You also deleted the listeners property from the virtual node.
Now write the removeEventListeners() function. This function, given an object of event names and event handlers, removes the event listeners from the element.
Recall that the listeners property of the virtual node is an object that maps event names to event handlers. The following could be an example listeners object for a virtual node representing a button:
{
mouseover: () => { ... },
click: () => { ... },
dblclick: () => { ... }
}
In this example, you’d have three event handlers to remove. For each of them, call the Element object’s removeEventListener() method (), which it inherits from the EventTarget interface:
el.removeEventListener('mouseover', listeners['mouseover'])
el.removeEventListener('click', listeners['click'])
el.removeEventListener('dblclick', listeners['dblclick'])
Open the events.js file, and fill in the missing code:
export function removeEventListeners(listeners = {}, el) {
Object.entries(listeners).forEach(([eventName, handler]) => {
el.removeEventListener(eventName, handler)
})
}
84
CHAPTER 4
Mounting and destroying the virtual DOM
Great! You’re missing only the code for destroying a fragment.
4.2.3
Destroying a fragment
Destroying a fragment is easy: simply call the destroyDOM() function for each of its children. But you have to be careful not to remove the el referenced in the fragment’s virtual node from the DOM; that el references the element where the fragment children are mounted, not the fragment itself. If the fragment children were mounted inside the
<body> and you called the remove() method on the element, you’d remove the whole document from the DOM. That’s not what you want to do—or do you? Figure 4.9 shows the problem more graphically.
Don’t remove this element
Fragment
from the DOM! It might be
el
part of the DOM you don’t
own.
<h1>
<p>
el
el
<body>
Remove
Remove
<h1>My Blog</h1>
from DOM
from DOM
<p>Welcome!</p>
Text
Text
</body>
el
el
"My Blog"
"Welcome!"
Figure 4.9
When you’re destroying a fragment, don’t remove its referenced element from the DOM. That element might be the <body> or some other element that you didn’t create and therefore don’t own.
The implementation of the removeFragmentNodes() is simple:
function removeFragmentNodes(vdom) {
const { children } = vdom
children.forEach(destroyDOM)
}
That’s it! You’ve implemented the mountDOM() and destroyDOM() functions. These functions, together with the state management system that you’ll implement in chapter 5, will be the core of the first version of your framework. You’ll use the framework to refactor the TODOs application.
Exercise 4.4
Using the destroyDOM() function, remove the section that you added to your local newspaper’s website (exercise 4.2) from the DOM. Make sure that the fragment’s referenced element isn’t removed from the DOM because it was created by the newspaper’s website, not by you. Then check the vdom tree you used to create the section and make sure that the el property has been removed from all the virtual nodes.
Find the solution .
Summary
85
The first version won’t be very sophisticated—it’ll destroy the entire DOM using destroyDOM() and mount it from scratch using mountDOM() every time the state changes—but it will be a great starting point. By the end of the next chapter, you’ll have a working framework, so I hope you’re excited. See you in chapter 5!
Summary
Mounting a virtual DOM means creating the DOM nodes that represent each virtual node in the tree and inserting them into the DOM, inside a parent element.
Destroying a virtual DOM means removing the DOM nodes created from it, making sure to remove the event listeners from the DOM nodes as well.
State management and
the application’s lifecycle
This chapter covers
Understanding state management
Implementing a state management solution
Mapping JavaScript events to commands that
change the state
Updating the state using reducer functions
Re-rendering the view when the state changes
Some time ago, I went to the coast in the south of Spain, to a small village in Cadiz.
One restaurant was so popular that it ran out of food quickly; it served a limited quantity of the dishes on the menu. As waiters took orders from customers, they updated a chalkboard with the number of servings the restaurant had left, and when no more of a particular dish was left, they crossed it out. The customers could easily tell what they could order by looking at the chalkboard. But from time to time, due to the workload, a waiter might forget to update the chalkboard, and customers would find out that the dish they’d been waiting in line so long to order was sold out. You can picture the drama. Clearly, it was important for the restaurant to have an updated chalkboard that matched the remaining servings of each dish.
86

87
To have a working framework, you’re missing a key piece that does a job similar to that of a waiter in that restaurant: a state manager. In the restaurant, a waiter is in charge of keeping the chalkboard in sync with the state of the restaurant: the number of servings of each dish still available. In a frontend application, the state changes as the user interacts with it, and the view needs to be updated to reflect those changes.
The state manager keeps the application’s state in sync with the view, responding to user input by modifying the state accordingly and notifying the renderer when the state has changed. The renderer is the entity in your framework that takes the virtual Document Object Model (DOM) and mounts it into the browser’s document.
In this chapter, you’ll implement both the renderer—using the mountDOM() and destroyDOM() functions from chapter 4—and the state manager entities, and you’ll learn how they communicate. By the end of the chapter, you’ll have your first version of the framework, the architecture of which (figure 5.1) is the state manager and renderer glued together. (The state manager is displayed with a question mark in the figure because you’ll discover how it works in this chapter.)
State manager
2. Read/write.
?
3. Notify renderer.
1. User action.
Renderer
State
destroyDOM( )
vdom
vdom state
= View( )
5. Re-render view.
4. Read.
mountDOM( , )
vdom parentEl
Figure 5.1
Your first framework is a state manager and a renderer glued together.
The framework you’ll have at the end of the chapter is rudimentary. To ensure that the view reflects the current state, its renderer destroys and mounts the DOM every time the state changes. As you can see in figure 5.1, the renderer draws the view in a three-step process:
1
Destroy the current DOM (calling destroyDOM()).
2
Produce the virtual DOM representing the view given the current state by calling the View() function, the top-level component.
3
Mount the virtual DOM into the real DOM by calling mountDOM().
Destroying and mounting the DOM from scratch is far from ideal, and we’ll discuss why in chapter 7. In any case, re-rendering the entire application is a good starting point. You’ll improve the rendering mechanism in chapters 7 and 8, where (thanks to



88
CHAPTER 5
State management and the application’s lifecycle
the reconciliation algorithm) your framework can update the DOM more efficiently.
You have to walk before you can run, they say.
How does implementing a renderer and state manager fit into the bigger picture of your framework? Let’s take a step back. If you recall from chapter 1, when the browser loads an application, the framework code renders the application’s view (step 5 in figure 5.2, reproduced from chapter 1 for convenience). The renderer can do this job alone; the state manager doesn’t intervene. (It might, but let’s leave that case for now.)
Idle server
The first render happens when
the framework mounts the app.
Browser storage
4. Framework reads the view’s components.
5. Framework creates the HTML programmatically.
js
css
js
html
Application bundle
Vendors bundle
Figure 5.2
Single-page application (SPA) first render in the browser
When the user interacts with the application (step 6 in figure 5.3, reproduced from chapter 1), the state manager updates the state and notifies the renderer to re-render the view. This dynamic is a bit more complex and requires the state manager and renderer to work together.
The state manager somehow needs to be aware of the events that the user interactions can trigger, such as clicking a button or typing in an input field, and it must know what to do with the state when those events take place. In a way, it needs to know everything the user might do with the application beforehand and how those actions will affect the state.



89
Idle server
Only the parts that change in the
6. User interacts with the
HTML are patched (re-rendered).
page. An event is emitted.
Browser storage
7. Framework executes the event handlers
defined in the application.
8. Framework patches the parts of the HTML
that change.
js
css
js
html
Application bundle
Vendors bundle
Figure 5.3
SPA responding to events
In this chapter, you’ll learn how to handle state changes in response to user input.
With the big picture in mind, let’s start working on the state manager.
NOTE
You can find all the listings in this chapter in the listings/ch05 directory of the book’s repository ). The code you write in this chapter is for the framework’s first version, which you’ll publish in chapter 6. Therefore, the code in this chapter can be checked out from the ch6
label ): $ git switch --detach ch6.
Code catch-up
In chapter 4, you implemented the mountDOM() and destroyDOM() functions.
mountDOM() takes a virtual DOM tree and a parent DOM element, and mounts the virtual DOM into the parent element. Its implementation is broken into a few subfunctions:
createTextNode()—To create HTML text nodes.
createElementNode()—To create HTML element nodes. This function uses two subfunctions: addEventListeners() and setAttributes().
createFragmentNodes()—To create lists of nodes that have a common parent.
90
CHAPTER 5
State management and the application’s lifecycle
(continued)
destroyDOM() takes a DOM element and removes it from the DOM. Its implementation is also broken down into a few subfunctions:
removeTextNode()—To remove HTML text nodes.
removeElementNode()—To remove HTML element nodes. This function uses another function to remove the element’s event listeners: removeEventListeners().
removeFragmentNodes()—To remove lists of nodes that have a common parent.
5.1
The state manager
Let’s study the chronology of everything that happens between the user’s interacting with the application and the view’s being updated. In the following list, the actor goes first, followed by the action it performs:
1
The user—Interacts with the application’s view (clicks a button, for example).
2
The browser—Dispatches a native JavaScript event such as MouseEvent) or KeyboardEvent (
3
The application developer—Programmed the framework to know how to update the state for each event.
4
The framework’s state manager—Updates the state according to the application developer’s instructions.
5
The framework’s state manager—Notifies the renderer that the state has changed.
6
The framework’s renderer—Re-renders the view with the new state.
This list isn’t exactly a chronology because the application developer doesn’t intervene between the user’s interaction with the application and the state manager’s update of the state. Instead, the developer gives instructions on updating the state during application development (not at run time). Still, this list is a good way to describe the flow of events. Figure 5.4 illustrates this pseudochronology in the architectural diagram of the framework I presented in figure 5.1.
This pseudochronology gives rise to two key questions: how does the application developer instruct the framework how to update the state when a particular event is dispatched, and how does the state manager execute those instructions? The answers to both questions are the keys to understanding the state manager.
5.1.1
From JavaScript events to application domain commands
The first thing you need to notice is that the JavaScript events dispatched by the browser don’t have a concrete meaning in the application domain on their own. The user clicked this button; so what? The application developer is the one who translates

5.1
The state manager
91
3. App developer writes instructions
4. Update state by
for updating the state given an
executing instructions.
event from the browser.
1. User interaction
2. JS event
State manager
Read/write
update state
Renderer
State
destroyDOM( )
vdom
vdom state
= View( )
Read
mountDOM( , )
vdom parentEl
5. State manager notifies the
6. Re-render view
renderer about the state change.
Figure 5.4
The pseudochronology of events in a frontend application
user actions into something meaningful for the application. Think about the TODOs application:
When the user clicked the Add button or pressed the Enter key, they wanted to add a new TODO item to the list.
“Adding a to-do” is framed in the language of the application, whereas “clicking a button” is a generic thing to do. (You click buttons in all sorts of applications, but that action translates to adding a to-do only in the TODOs application.) If the application developer wants to update the state when a particular event is dispatched, first they need to determine what that event means in terms of the application domain. Then the developer maps the event to a command that the framework can understand. A command is a request to do something, as opposed to an event, which is a notification of something that has happened. These commands ask the framework to update the state; they are expressed in the domain language of the application.
Events vs. commands
An event is a notification of something that has happened. “A button was clicked,” “A key was pressed,” and “A network request was completed” are examples of events.
Events don’t ask the framework or application to do anything; they’re simply notifications with some additional information. Event names are usually framed in past tense:
'button-clicked', 'key-pressed', 'network-request-completed', and so on.
92
CHAPTER 5
State management and the application’s lifecycle
(continued)
A command is a request to do something in a particular context. “Add todo,” “Edit todo,” and “Remove todo” are three examples of commands. Commands are written in imperative tense because they’re requests to do something: 'add-todo', 'edit-todo', 'remove-todo', and so on.
Continuing the TODOs application example, table 5.1 lists a few events that the user can trigger and the commands that the application developer would dispatch to the framework in response to those events.
Table 5.1
Mapping between browser events and application commands in the TODOs application Browser event
Command
Explanation
Click the Add button.
add-todo
Clicking the Add button adds a new to-do item
to the list.
Press the Enter key (while
add-todo
Pressing the Enter key adds a new to-do item
the input field is focused).
to the list.
Click the Done button.
remove-todo
Clicking the Done button marks the to-do item
as done, removing it from the list.
Double-click a to-do item.
start-editing-todo
Double-clicking a to-do item sets the to-do item
in edit mode.
In figure 5.5, you see the same mapping. In it, you can establish a link between the browser events and the application commands, which the application developer dispatched to the framework by using the dispatch() function. You’ll learn more about this function in section 5.1.3.
My TODOs
'keydown' : dispatch('add-todo')
New TODO
Add
'click' : dispatch('add-todo')
- Walk the dog
Done
'click' : dispatch('remove-todo')
'dblclick' : dispatch('start-editing-todo')
Figure 5.5
The mapping between browser events and application commands
5.1
The state manager
93
How does this figure answer the preceding questions? After the application domain commands are identified, the application developer can supply functions that update the state as a response to those commands. The state manager executes those functions to update the state. In section 5.1.2, we’ll look into what these functions are and how they’re executed.
Exercise 5.1
Imagine an application that consists of a counter and two buttons: one to increment the counter and another one to decrement it. Its HTML could look like this:
<button>-</button>
<span>0</span>
<button>+</button>
When each button is clicked, what commands would you dispatch to the framework?
Can you draw a diagram similar to the one in figure 5.5 that maps browser events to application commands?
Find the solution at
5.1.2
The reducer functions
Reducer functions can be implemented in a few ways. But if we decide to stick to the functional programming principles of using pure functions and making data immutable, instead of updating the state by mutating it, these functions should create a new one (figure 5.6).
A function that, given the current
state and the payload of a command,
returns a new, updated state
reduce( , ) =
Current state
Command payload
New state
Figure 5.6
A reducer function takes the current state and a payload and returns a new state.
NOTE
This process may sound familiar if you’ve used Redux (
: these functions are the reducers. (The term reducer comes from the reduce function in functional programming.) A reducer, in our context, is a function that takes the current state and a payload (the command’s data) and returns a new updated state. These functions never mutate the state that’s
94
CHAPTER 5
State management and the application’s lifecycle
passed to them (mutation would be a side effect, so the function wouldn’t be pure); instead, they create a new state.
Consider an example based on the TODOs application. To create a new version of the state when the user removes a to-do item from the list (recall from chapter 2 that the state was the list of to-dos), the reducer function associated with this 'remove-todo'
command would look like this:
function removeTodo(state, todoIndex) {
return state.toSpliced(todoIndex, 1)
}
If we had the state
let todos = ['Walk the dog', 'Water the plants', 'Sand the chairs']
and wanted to remove the to-do item at index 1, we would compute the new state by using the removeTodo() reducer, as follows:
todos = removeTodo(todos, 1)
// todos = ['Walk the dog', 'Sand the chairs']
In this case, the payload associated with the 'remove-todo' command is the index of the to-do item to remove. Note that the original array is not mutated; a new one is created instead. The toSpliced() method of an array returns a new array.
Exercise 5.2: Challenge
Suppose that you’re building a tic-tac-toe game (as a web application. You have a 3x3 grid of squares, and when one of the two players clicks one of them, you want to mark it with an X (cross) or an O (nought). The player who places three of their marks in a horizontal, vertical, or diagonal row wins the game.
How would you design the state of the application? What commands would you dispatch to the framework in response to the user’s clicking a square? What reducer functions would you implement to update the state? Here are some tips to help you get started:
The state needs to have at least four pieces of information: the grid of squares, the player whose turn it is, the winner of the game (if any), and whether the game ended in a draw.
To design what commands are needed, think about what actions the user can perform in the application.
You can deduce the reducers from the commands, as well as how the state needs to be updated in response to them.
Find the solution .
5.1
The state manager
95
Let’s do a quick recap. We’ve seen that the application developer translates browser events into application domain commands and that the state manager executes the reducer functions associated with those commands to update the state. But how does the state manager know which reducer function to execute when a command is dispatched? Something has to map the commands to the reducer functions. We’ll call this mechanism a dispatcher; it’s the state manager’s central piece.
5.1.3
The dispatcher
The association between commands and reducer functions is performed by an entity we’ll call the dispatcher. The name reflects the fact that this entity is responsible for dispatching the commands to the functions that handle the command—that is, for executing the corresponding handler functions in response to commands. To do this, the application developer must specify which handler function (or functions) the system should execute in response to each command.
These command handler functions are consumers. Consumer is the technical term for a function that accepts a single parameter—the command’s payload, in this case—
and returns no value, as figure 5.7 shows.
A function that accepts a single
parameter and returns no value
consume( ) = void
Payload
Figure 5.7
A consumer function takes a single parameter and returns no value.
A consumer that handles a command can easily wrap a reducer function, as the following example shows:
function removeTodoHandler(todoIndex) {
// Calls the removeTodo() reducer function to update the state.
state = removeTodo(state, todoIndex)
}
As you can see, the command-handler function that removes a to-do from the list receives the to-do index as its single parameter and then calls the removeTodo() reducer function to update the state. The handler simply wraps the reducer function; it’s the dispatcher’s responsibility to execute the handler function in response to the
'remove-todo' command. But how do you tell the dispatcher which handler function to execute in response to a command?
96
CHAPTER 5
State management and the application’s lifecycle
ASSIGNING HANDLERS TO COMMANDS
Your dispatcher needs to have a subscribe() method that registers a consumer function—the handler—to respond to commands with a given name. The same way that you can register a handler for a command, you can unregister it when it doesn’t need to be executed anymore (because the relevant view has been removed from the DOM, for example). To accomplish this task, the subscribe() method should return a function that can be called to unregister the handler.
Your dispatcher also needs to have a dispatch() method that executes the handler functions associated with a command. Figure 5.8 shows how the dispatcher works.
2. The consumer is added as
handler for the 'ghi' command.
1. A consumer is subscribed to
Dispatcher
handle a command named 'ghi.'
Command handler registry
subscribe('ghi', consumer)
'abc'
f(payload)
'def'
f(payload)
dispatch('ghi', { ... })
Command
'ghi'
f(payload)
3. Dispatch a command with
a payload associated.
4. The dispatcher looks up the command
name and executes the associated
consumers, passing them the payload.
Figure 5.8
The dispatcher’s subscribe() method registers handlers to respond to commands with a given name, and dispatch() executes the handlers associated with a command.
It’s time to implement the dispatcher. Create a new file called dispatcher.js in the src/
directory. Your runtime/src/ directory should look like this (new file in bold): src/
├── utils/
│ └── arrays.js
├── attributes.js
├── destroy-dom.js
├── dispatcher.js
├── events.js
├── h.js
├── index.js
└── mount-dom.js
Now write the code in listing 5.1.
5.1
The state manager
97
NOTE
The hash (#) in front of the variable name is the ES2020 way to make the variable private inside a class. Starting with ES2020, any variable or method that starts with a hash is private and can be accessed only from within the class.
Listing 5.1
Registering handlers to respond to commands (dispatcher.js)
export class Dispatcher {
#subs = new Map()
Creates the array of subscriptions
if it doesn’t exist for a given
subscribe(commandName, handler) {
command name
if (!this.#subs.has(commandName)) {
this.#subs.set(commandName, [])
}
const handlers = this.#subs.get(commandName)
if (handlers.includes(handler)) {
Checks whether the
return () => {}
handler is registered
}
handlers.push(handler)
Registers the handler
return () => {
Returns a function to
const idx = handlers.indexOf(handler)
unregister the handler
handlers.splice(idx, 1)
}
}
}
The Dispatcher is a class with a private variable called subs (short for subscriptions): a JavaScript map ( to store the registered handlers by event name.
Note that more than one handler can be registered for the same command name.
The subscribe() method takes a command name and a handler function as parameters, checks for an entry in subs for that command name, and creates an entry with an empty array if one doesn’t exist. Then it appends the handler to the array in case it wasn’t already registered. If the handler was already registered, you simply return a function that does nothing because there’s nothing to unregister.
If the handler function was registered, the subscribe() method returns a function that removes the handler from the corresponding array of handlers, so it’s never notified again. First, you look for the index of the handler in the array; then you call its splice() method to remove the element at that index. Note that the index lookup happens only when the returned function—the unregistering function—is called. This detail is very important because if you did the lookup outside that function—inside the subscribe() method body—the index that you’d get might not be valid by the time you want to unregister the handler. The array might have changed in the meantime.
WARNING
When the handler is already registered, instead of returning an empty function, you may be tempted to return a function that unregisters the existing handler. But returning an empty function is a better idea because it prevents the side effects that result when a developer inadvertently calls the
98
CHAPTER 5
State management and the application’s lifecycle
returned function twice—once for each time they called subscribe() using the same handler. In this case, when the same handler is unregistered for the second time, indexOf() returns -1 because the handler isn’t in the array anymore.
Then the splice() function is called with an index of -1, which removes the last handler in the array—not what you want. This silent failure (something going wrong without throwing an exception) is something you want to avoid at all costs, as debugging these kinds of problems can be a nightmare.
Now that you’ve implemented the dispatcher’s first method, subscribe(), how does the dispatcher tell the renderer about state changes? We’ll try to find the answer in the next section.
NOTIFYING THE RENDERER ABOUT STATE CHANGES
At the beginning of this chapter, I said that the state manager is in charge of keeping the state in sync with the views. It does so by notifying the renderer about state changes so that the renderer can update the views accordingly. Then how does the dispatcher notify the renderer?
You know that the state can change only in response to commands. A command triggers the execution of one or more handler functions, which execute reducers, which in turn update the state. Therefore, the best time to notify the renderer about state changes is after the handlers for a given command have been executed. You should allow the dispatcher to register special handler functions (we’ll call them after-command handlers) that are executed after the handlers for any dispatched command have been executed. The framework uses these handlers to notify the renderer about potential state changes so that it can update the view.
Figure 5.9 shows the afterEveryCommand() method as part of the dispatcher’s architecture. The functions registered with this method are called after every command is handled; you can use them to notify the renderer about state changes.
In the following listing, write the code in bold inside the Dispatcher class to add the afterEveryCommand() method.
Listing 5.2
Registering functions to run after commands (dispatcher.js)
export class Dispatcher {
#subs = new Map()
#afterHandlers = []
// --snip-- //
afterEveryCommand(handler) {
Registers the handler
this.#afterHandlers.push(handler)
return () => {
Returns a function to
const idx = this.#afterHandlers.indexOf(handler)
unregister the handler
this.#afterHandlers.splice(idx, 1)