Skip to main content Accessibility Feedback

(mostly) vanilla JS DOM diffing and data reactivity

Yesterday, we learned how state-based UI works. We also bumped into some of the main issues with entirely vanilla JS state-based UI.

Destroying and rewriting the entire DOM is bad for both performance and accessibility. And you need to remember to rerender the UI whenever your data changes.

Today, we’re going to learn about two of the main benefits that most state-based UI libraries provide: DOM diffing and data reactivity.

Let’s dig in!

An (almost) vanilla JS state-based UI library

For today’s article, we’re going to use Reef, my tiny state-based UI library.

It weighs just 2kb minified and gzipped, and provides an entirely vanilla JS authoring experience. You don’t need any build tools or compilers, and you don’t need to learn any special pseudo-languages like JSX.

To install it, we’ll load it directly from the CDN just like any other JS file.

<script src="https://cdn.jsdelivr.net/npm/reefjs@12/dist/reef.min.js"></script>

Reef has a handful of methods that we’ll be learning about shortly. We can access them directly on the reef object, like this…

reef.render('#app', template);

But my preferred way is to use object destructuring to pull the methods I want into their own variables.

let {render} = reef;

render('#app', template);

It provides an ES module-like experience, without having to deal with ES modules (those are also an option, if you’d prefer).

DOM diffing

The big thing that Reef handles is DOM diffing.

Let’s imagine that the current UI looks like this.

<div id="app">
	<p><em>You don't have any todos yet.</em></p>
</div>

The user enters their first todo item, and the new UI should look like this…

<div id="app">
	<ul>
		<li>Buy milk <button data-delete="0">Delete</button></li>
	</ul>
</div>

Rather than completely wiping out the contents of the #app element, Reef compares what’s currently there against the desired HTML and makes just the changes that are needed.

In this case it would…

  • Remove the paragraph element (p)
  • Create an unordered list element (ul)
  • Add the child list item (li) and its contents
  • Append the list into the #app element

Let’s say the user adds another list item. Now the UI should look like this…

<div id="app">
	<ul>
		<li>Buy milk <button data-delete="0">Delete</button></li>
		<li>Rewatch the Expand on Amazon <button data-delete="1">Delete</button></li>
	</ul>
</div>

Rather than completely wipe out the existing list and create a new one, Reef would create a new list item (li) and append it to the end of the existing list.

The user deletes an item? Same thing! Reef surgically removes just that one item.

State-based UI libraries do the old-school DOM manipulation under-the-hood so that you don’t have to figure out what needs to change.

Using DOM diffing with Reef

To use DOM diffing with Reef, we need the render() function.

let {render} = reef;

You pass the element to diff (or a selector string for that element) and the HTML string to render. Reef figures out the rest.

In our todo app, instead of this…

app.innerHTML = getHTML();

We would do this…

render(app, getHTML());

Here’s a demo for you on CodePen.

Data reactivity

The other big thing most state-based UI libraries offer is data reactivity.

That means that when you update your state object, the UI is automatically diffed and updated if there are any changes.

That means that instead of doing this…

// Otherwise, add todo and rerender the UI
todos.push(form.todo.value);
render(app, getHTML());

You can just do this…

// Otherwise, add todo and rerender the UI
todos.push(form.todo.value);

The act of modifying the todos array triggers the UI to diff and rerender.

Using data reactivity with Reef

Different libraries handle reactivity in different ways.

Reef and Vue uses proxies to track changes to arrays and objects. React, Preact, Solid and many others use a setter function, a special function you have to run to update your values.

To use data reactivity with Reef, we need to the store() and component() functions. We no longer need render().

let {store, component} = reef;

We can turn our todos array into a reactive array by passing it into the store() method.

// Reactive todo data
let todos = store(JSON.parse(localStorage.getItem('todos-reef')) || []);

Whenever we update its value, it emits an event, reef:store, that you can listen for with the addEventListener() method.

document.addEventListener('reef:render', function () {
	console.log('Our data was updated!');
});

To automatically render our UI whenever that happens, we can use the component() method.

Pass in the element to render into and the template function to run (without parentheses). The component() method will listen for reef:store events and automatically run the render() function under-the-hood for us to diff the DOM.

component(app, getHTML);

Now, instead of manually running the render() function, we can just push and delete data from the todos array, and the UI will update to match.

Here’s another demo.

Hooking into events

Most state-based UI libraries provide a few additional benefits that make writing code just a little nicer.

With Reef, one of those is event hooks.

Reef emits a handful of events whenever things happen. One of the most useful is the reef:render event, which fires every time the UI is updated.

In our todo app, we can setup a listener for the reef:render event, and save our todos array to localStorage whenever that happens instead of manually saving it in multiple places.

Instead of doing this…

// Otherwise, remove the todo
todos.splice(index, 1);

// Save the list
localStorage.setItem('todos-reef', JSON.stringify(todos));

We can just do this…

// Otherwise, remove the todo
todos.splice(index, 1);

And then setup a listener to handling the saving for us.

// Save todos whenever a render happens
document.addEventListener('reef:render', function (event) {
	localStorage.setItem('todos-reef', JSON.stringify(todos));
});

Here’s one last demo.

How does this work in other libraries?

You may be onboard with this approach, but convincing the rest of your team to ditch their beloved React or Vue for some tiny open source project is typically a hard sell.

Tomorrow, I’m going to show you how to get a very similar authoring experience to what we did here using Preact.

Preact has the same API as React, but it’s 1/10th the size, loads and renders the UI faster, is widely used, and has great documentation. And best of all, you can use it with just template literals. No need for JSX (though you can use it if you want to).