Advanced Topics

VanJS: Advanced Topics

DOM Attributes vs. Properties

In tag functions, while assigning values from props parameter to the created HTML element, there are 2 ways of doing it: via HTML attributes (dom.setAttribute(<key>, <value>)), or via the properties of the created HTML element (dom[<key>] = <value>). VanJS follows a consistent rule that makes sense for most use cases regarding which option to choose: when a settable property exists in a given <key> for the specific element type, we will assign the value via property, otherwise we will assign the value via attribute.

For instance, input({type: "text", value: "Hello 🍦VanJS"}) will create an input box with Hello 🍦VanJS as the value of the value property, while div({"data-index": 1}) will create the tag: <div data-index="1"></div>.

Note that, for readonly properties of HTML elements, we will still assign props values via setAttribute. For instance, in the code snippet below, the list of the <input> element is set via setAttribute:

const Datalist = () => div(
  label({for: "ice-cream-choice"}, "Choose a flavor: "),
    list: "ice-cream-flavors",
    id: "ice-cream-choice",
    name: "ice-cream-choice",
    {id: "ice-cream-flavors"},
    option({value: "Chocolate"}),
    option({value: "Coconut"}),
    option({value: "Mint"}),
    option({value: "Strawberry"}),
    option({value: "Vanilla"}),

Try on jsfiddle

Garbage Collection

There is garbage collection mechanism implemented in VanJS to recycle obsolete state bindings. To illustrate the necessity of garbage collection, let's take a look at the code below:

const renderPre = van.state(false)
const text = van.state("Text")
const TextLine = (renderPre: boolean) =>
  (renderPre ? pre : div)(
    van.bind(text, t => `--${t}--`),
const dom = div(van.bind(renderPre, TextLine))

In this piece of code, we have created an element dom, whose only child is bound to a boolean state - renderPre, which determines whether dom has a <pre> or <div> child element. Inside the child element, the underlying text is bound to a string state - text. Whenever the value of renderPre is toggled, a new version of the DOM tree will be generated, and we will add a new binding from text state to the text node of the newly created DOM tree.

Without proper garbage collection implemented, text state will eventually be bound to many text nodes after renderPre is toggled many times. All the of bindings, except the most recently added one, are actually obsolete, as they bind the text state to a text node that is not currently being used. i.e.: disconnected from the document tree. Meanwhile, because internally, a State object holds reference to all DOM elements that are bound to it, these DOM elements won't be GC-ed by JavaScript runtime, causing memory leaks.

Garbage collection is implemented in VanJS to resolve the issue. There are 2 ways a garbage collection activity can be triggered:

  1. Periodic recycling: periodically, VanJS will scan all State objects that have new bindings added recently, and remove all the bindings to a disconnected DOM element. i.e.: isConnected property is false.
  2. Pre-rendering recycling: before VanJS re-render the DOM tree in response to state changes, it will first check all the states whose values have been changed in this render cycle, and remove all the bindings to a disconnected DOM element.

Avoid your bindings to be GC-ed unexpectedly

There are some general guidelines to follow to avoid your bindings being garbage collected unexpectedly:

  1. Please complete the construction of the DOM tree and connect the newly constructed DOM tree to the document object before making any state changes. Otherwise, the bindings to yet-to-be-connected DOM elements will be garbage collected.
  2. DOM tree construction needs to be synchronous. i.e.: you shouldn't have any suspension point while building the DOM tree (e.g.: await something in an async function). Otherwise, periodic recycling might be scheduled in the middle of the suspension point which can cause bindings to yet-to-be-connected DOM elements being garbage collected.

onnew listeners are not subject to GC

Note that, the garbage collection in VanJS only removes obsolete bindings. It doesn't apply to event handlers registered via onnew method. For instance, the code below still suffers from memory leaks:

const renderPre = van.state(false)
const text = van.state("Text")
const TextLine = (renderPre: boolean) => {
  const expandedText = van.state("--Text--")
  text.onnew(t => expandedText.val = `--${t}--`)
  return (renderPre ? pre : div)(expandedText)
const dom = div(van.bind(renderPre, TextLine))

In this example, whenever the generation function TextLine is called, a new State object will be created and subscribe to the change of the text state. Because every event handler registered via onnew holds the reference to local State variable expandedText, the instances of expandedText variable will not be GC-ed by JavaScript runtime even when they are no longer being actively used.

To avoid memory leaks caused by onnew, in the generation function of the van.bind call, you shall NEVER register event handlers via onnew method for State objects defined outside the scope of generation function.