matthewp/views-the-hard-way: JavaScript views; done as painfully as possible

by oqtey
matthewp/views-the-hard-way: JavaScript views; done as painfully as possible

Learn how to build views in plain JavaScript in a way that is maintainable, performant, and fun. Writing JavaScript Views the Hard Way is inspired by such books as Learn C the Hard Way.

Writing JavaScript Views the Hard Way is a pattern for writing JavaScript views. It is meant to serve as an alternative to using frameworks and libraries such as React, Vue and lit-html.

It is a pattern, not a library. This document explains how to write views in such a way as to avoid the spaghetti code problems that commonly occur when writing low-level imperative code.

We call this technique the hard way because it askews abstractions in favor of directness.

Advantages over frameworks

There are several reasons why you might be interested in writing your views the hard way:

  • Performance: Writing JavaScript Views the Hard Way uses direct imperative code, so there are no unnecessary operations performed. Whether a hot or cold path, using this technique ensures nearly the best possible performance you can get in JavaScript.
  • 0 dependencies: This technique uses no dependencies, so your code will never have to be upgraded. Have you ever used a library that released a breaking change that took you a day to upgrade? You’ll never experience that problem again.
  • Portability: Code written with simple imperative views is portable to any framework. That makes it perfect for low-level components that you might want to share with several framework communities. But I recommend using it on full apps as well.
  • Maintainability: Despite the reputation of imperative code being difficult to maintain, views written with Writing JavaScript Views the Hard Way are extremely maintainable. This is because they follow strict conventions (you’ll learn these later). These conventions ensure you always know where to look in a view. Additionally it follows a props down, events up model that makes data sharing straight-forward.
  • Browser support: Code written in this manner is supported by all browsers; full-stop. We do use events to make passing data back up the component tree and our examples use a newer, nicer API, to do that, but you can use an older technique (discussed in the compatibility section) to get you back to at least IE9. But if you want to go further back than that even, substitute passing functions as props instead of using events and you can use this technique in IE6 if you want. And it will be by far the most performant solution you’ll find for old browsers.
  • Easier to debug: Using this approach stack traces become shallow (usually only a few function calls). This is because there are no layers between events and your code. Everything is your code, and as long as you name your functions, you’ll get incredible stack traces that make it easy to trace where something goes wrong.
  • Functional: This doesn’t differentiate the technique vs all frameworks but it’s worth pointing out at a benefit. Writing JavaScript Views the Hard Way is not functional in the immutable sense; there are definitely mutations; but it is functional in the sense that you’re dealing with plain functions (no classes in sight) and without side-effects outside of the view’s local state.

Enough with the arguments for now, let’s talk about the structure. A view component written with Writing JavaScript Views the Hard Way looks like the following. This is a full hello world. From here we’ll break down each part and explain it on its own.

Once you understand each part you’ll know how to build components/views using this pattern; it’s everything you need to know.

const template = document.createElement('template');
template.innerHTML = `
  

Hello world!

`; function clone() { return document.importNode(template.content, true); } function init() { /* DOM variables */ let frag = clone(); let nameNode = frag.querySelector('#name'); /* State variables */ let name; /* DOM update functions */ function setNameNode(value) { nameNode.textContent = value; } /* State update functions */ function setName(value) { if(name !== value) { name = value; setNameNode(value); } } /* State logic */ /* Event dispatchers */ /* Event listeners */ /* Initialization */ function update(data = {}) { if(data.name) setName(data.name); return frag; } return update; } export default init;

This is the basic structure. More details are to follow. First let’s concentrate on the module’s parts and exports.

const template = document.createElement('template');
template.innerHTML = `
  

Hello world!

`;

This is the view’s template. It’s a