I have an interactive tutorial that takes you step by step to make an interactive tutorial in d3.js and another one in vue.js. However, the code for those is more involved than I’d like, so I wanted to make something even simpler. I wrote the same example with several libraries and made some notes for myself. I often use these as a starting point when I’m working on a project.
None of these examples go very far, such as using components or multiple modules. I’m primarily evaluating these for my interactive diagrams and not for making “web apps”.
1 Vanilla JavaScript#
No libraries, no build step.
Notes:
- Output is imperative style.
- Use createElement and appendChild, with setAttribute to set attributes and createTextNode to set text.
- Create and update are separate code. You have to keep track of whether something’s already created.
- Input is with addEventListener and callback functions.
- Redraw is explicit. Call redraw every time you change data.
- Modularity through your own abstractions in code.
A simpler alternative to createElement etc. is to construct a string and set innerHTML
. This is nice unless you have event listeners on those elements. Setting innerHTML
creates new elements. If you are dragging the mouse based on an event handler, and the mouse drag replaces the elements, that mouse drag might be running on an element that no longer exists, and that can be tricky to debug. You’ll also have to be careful to escape your HTML correctly if you are setting innerHTML
.
2 Vue#
Medium sized input&output library (80k), no build step required, but Vue-specific compiler optional, and Vue-agnostic JSX also allowed but not widely used.
- Vue 2 w/HTML demo and code[2]
- Vue 3 w/HTML demo and code[3]
- Vue 3 w/JSX demo and code[4]
- Vue 3 w/SFC demo and code[5]
Notes:
- Output is template style, described with html syntax in the html file or in the JavaScript file.
- Use :attr="value" to set attributes and {{value}} to set text.
- Use v-if and v-for for branch/loop in templates.
- Create and update are unified. The system keeps track of whether something’s already created.
- Input is with @click in the html to attach a click event handler.
- Redraw is automatic for state shared with Vue. Run this.x = 5 and Vue will automatically redraw. If you pass an object into Vue, you can still modify the state outside of Vue and it will pick it up.
- Modularity through custom elements like <my-gridworld>.
Tricky:
- need to use view-box.camel instead of viewBox because of naming convention differences (js uses camel case; html uses kebab case, except for this property); only applies when writing the template in the html, and not when writing the template in JavaScript.
- Vue 2 has some limitations on automatic tracking of updates. It tracks assignment to object fields expr.field = … including when the objects are inside other objects or arrays. It does not track assignment to array elements expr[index] = … but does handle array methods such as
push
andsplice
. Vue 2 does not know about the newSet
orMap
types. Vue 3 handles all of these situations. - Vue 3 is not backwards compatible with existing Vue 2 code; the Vue 2 code has to be migrated to work with Vue 3.
- Vue 3 still has a few limitations on automatic tracking. It cannot observe private fields, and I believe it cannot observe fields modified through inheritance chains.
- Vue supports Options API, Composition API with setup(), Composition API with
<script setup>
, render functions, JSX, and single file components. This means the ecosystem including docs, blogs, sample code, etc. is a bit fragmented. On the other hand, it’s this flexibility that allows me to adapt it to my non-webapp needs.
I am avoiding framework-specific build steps because it doesn’t match my long term goals, so I am not planning to use Vue’s Single-File Components (SFC) format, including <script setup>
. I use Vue without that format.
3 Lit#
Lit: small output library (11–16k), no build step required.
Notes:
- Output is template style, described with html`…` in html syntax in the JavaScript file.
- Use attr=${value} to set attributes and ${value} to set text.
- Use JavaScript logic for branch/loop in templates.
- Create and update are unified. The system keeps track of whether something’s already created.
- Input is with @click in the html to attach a click event handler.
- Redraw is explicit. Call redraw() every time you change data.
- Modularity through your own abstractions in code.
- Lit v1 doesn’t have a bundled version, but Lit v2 does.
- In Lit v2, when you change a property of a custom element (but not its nested properties), it re-renders the element.
Tricky:
- need to use svg`` for svg elements instead of html``.
- in html5 syntax, tags don’t self-close, so <element /> is only the open tag, and you still have to close it with </element>.
- Lit’s automatic re-rendering when changing a property only works for top level property changes, not changes to nested properties or array elements[8], so you still have to manually re-render for anything other than the top level. This is like Svelte. Vue on the other hand does automatically monitor all nested changes.
4 React#
Medium sized input&output library (140k), React-agnostic build step strongly recommended to convert JSX code into regular JavaScript.
- React 16 demo and code[9] (showing how to run it without a build step)
- React 18 demo and code[10] (shows the build step to compile jsx to js)
Notes:
- Output is template style, described with xml syntax (jsx) in the JavaScript file.
- Use attr={value} to set attributes and {value} to set text.
- Use JavaScript logic for branch/loop in templates.
- Create and update are unified. The system keeps track of whether something’s already created.
- Input is with onClick in the html to attach a click event handler.
- Redraw is automatic for state kept inside React’s components. Call this.setState({x: 5}) and React will redraw. Redraw is manual for state kept outside of React. Copy it into React every time you change something so that React will see it.
- Modularity through custom elements like <Gridworld>.
- React Native lets you build native apps (especially for phones).
Tricky:
- need to use the JavaScript names of elements and attributes instead of the html names. For example, use className=… instead of class=… like you would in html.
- need to convert SVG kebab case attributes like fill-opacity=… into camel case for JSX like fillOpacity=… because JSX doesn’t support the original names. This means you can’t copy an SVG file from a visual editor into JSX and have it work.
- there are also other html elements that have to be changed to work with React, such as for=… becoming htmlFor=… and tabindex=… being changed to tabIndex=… ; see list[11].
- some but not all input elements need to use onChange=… instead of onInput=… like HTML5 uses, and there doesn’t seem to be a way to get HTML5’s
onChange
. - when using the components, setState({x: 5}) doesn’t immediately update
x
to 5 (this is unlike Vue, which does immediately update, making the logic simpler); also see this explanation[12] of why
5 Preact#
Small input&output library (13k), build step if using JSX, no build step needed using HTM (+2k).
- Preact v10 + HTM demo and code[13] (HTM requires no build step)
- Preact v10 + JSX demo and code[14] (JSX requires a build step)
Notes:
- Output is template style, described with html`…` in xml syntax in the JavaScript file.
- Use attr=${value} to set attributes and ${value} to set text.
- Use JavaScript logic for branch/loop in templates.
- Create and update are unified. The system keeps track of whether something’s already created.
- Input is with onClick in the html to attach a click event handler.
- Redraw is automatic for state kept inside Preact’s components. Call this.setState({x: 5}) and React will redraw. Redraw is manual for state kept outside of React. Copy it into React every time you change something so that React will see it.
- Modularity through custom elements like <Gridworld>.
Preact is like React, but without the tricky items I listed under React: it allows html names like class=… rather than JavaScript names like className=…; and it allows svg names like fill-opacity=… rather than fillOpacity like React requires. This means you can use an SVG visual editor and export it directly into Preact. It more closely follows standard HTML, so you can both onInput and onChange events.
It normally uses JSX like React does but Preact’s HTM is like lit-html’s format, without the tricky items I listed under lit-html: you don’t have to have both html`` and svg``; the templates support xml syntax; and there’s a prebundled version of the library.
Tricky:
- like React, setState doesn’t trigger right away, so reading the state right after setting a new value can give you the old value[15]
6 Svelte#
Medium sized input&output library, build step required to convert Svelte code into regular JavaScript
I did not include a demo here, because it requires its own Svelte-specific compiler, and the Svelte playground didn’t let me share a link. However I mention Svelte here because I’ve heard good things about it, and it’s worth considering.
- Svelte v3 code[16]
Notes:
- Output is template style, described with html syntax in its own Svelte code file.
- Use attr={value} to set attributes and {value} to set text.
- Use #{if} and {#each} for branch/loop in templates.
- Create and update are unified. The system keeps track of whether something’s already created.
- Input is with on:click in the html to attach a click event handler.
- Redraw is automatic for state kept inside Svelte. It tracks top level changes to your underlying data and automatically redraws.
- Modularity through custom elements like <Gridworld>.
Tricky:
- Svelte tracks changes to top level variables based on assignment statements, but not to changes made to properties of objects or elements of arrays. See their tutorial[17] for workarounds. In my example code, I had to use this workaround on
gridWorld
. - Svelte does simple static analysis to find dependencies. See the
yPlusAValue
example in the documentation[18], in whichtotal
depends on bothx
andy
. Svelte detects only changes tox
, not changes toy
, even though both are reactive. Refactoring code can make Svelte lose track of reactivity. In my example code, I had to use a workaround onclassFor
. (They realized this and are switching to dynamic analysis for Svelte 5[19].)s
I am avoiding framework-specific build steps because it doesn’t match my long term goals, so I am not planning to use Svelte.
7 ObservableHQ#
Notebook style interface, where top level definitions become reactive in other expressions. Think spreadsheets.
Demo[20] partially implemented ; see source by clicking to the left of any cell.
- Output is template style, described with html`…` in the notebook
- Use attr=${value} to set attributes and ${value} to set text.
- Redraw is automatic for top-level definitions.
Tricky:
- custom viewof syntax if you want to have output cells also provide input (e.g. drawing on the grid)
8 My thoughts#
The main idea with templates is that instead of writing commands to generate html, we describe the html we want, with some placeholders for values that come from JavaScript values. For example:
<rect fill=red x=${col} y=${row} width=1 height=1 />
Compare this to the vanilla approach:
let rect = document.createElementNS("http://www.w3.org/2000/svg", 'rect'); rect.setAttribute("fill", "red"); rect.setAttribute("x", col); rect.setAttribute("y", row); rect.setAttribute("width", 1); rect.setAttribute("height", 1); svg.appendChild(rect);
or the d3.js approach:
let rect = svg.append("rect") .attr("fill", "red") .attr("x", col) .attr("y", row) .attr("width", 1) .attr("height", 1);
I find templates to be a big win. The major libraries in this space (React, Vue, Svelte, Preact, lit-html) all use templates, but the details differ.
<!-- react/preact/vue with jsx --> <rect fill=red x={col} y={row} width=1 height=1 /> <!-- vue templates --> <rect fill=red :x="col" :y="row" width=1 height=1 /> <!-- lit-html, and react/preact with htm --> <rect fill=red x=${col} y=${row} width=1 height=1 />
There’s some difference in how the templates are written. React uses an extension of JavaScript called JSX to allow you to write html in your JavaScript. You run a React-agnostic compiler to translate that into regular JavaScript. Vue reads HTML from your document, or in strings in the source code, or you can use a Vue-specific compiler to compile Vue “SFC” syntax. Lit-html uses a relatively new feature, JavaScript template literals. Preact normally uses JSX but there’s an option to use HTM template literals. Svelte uses a Svelte-specific compiler to compile code into regular JavaScript.
In addition, React, Preact, Vue, and Svelte offer a component system that allows you to create custom elements like <GridWorld> that are then expanded into HTML. Lit-html doesn’t do this, and instead leaves that to a separate library, LitElement. For my small projects, the component system doesn’t help me, as I can use regular JavaScript functions and classes instead. However, for larger projects, it provides some modularity and also allows you to reuse components that others have written. LitElement uses standard web components that can be used with any other system, whereas React, Preact, Vue, Svelte components can only be used within their own system.
https://component-party.dev/[21] shows a comparison of the syntax used across Svelte, React, Vue, and others.
For my own projects, I want to be able to use libraries without a build step and/or without node.js. Although React can be used without a build step, it’s designed to be used with one, and the docs not only say you need to use node.js and a build step, they also recommend you adopt a bigger framework. Svelte also requires a build step. Vue, Preact, and Lit prominently mention being able to run without a build step in their “getting started” docs. I use Vue most often in my projects, and have a tutorial showing how I use it for interactive diagrams.