v0.15.148 - 31.5 / 23.4

Anatomy of a Signals Based Reactive Renderer

We are going to implement from scratch the core parts of a signals based reactive web renderer: rendering, placeholders, node creation and node disposal.

In case you dont know what Signals are, here there are a few links:

  1. Ryan K Carniato - Revolutionary Signals (video)
  2. A Hands-on Introduction to Fine-Grained Reactivity
  3. Making the Case for Signals in JavaScript

# Introduction

With Signals, it may be a bit confusing at first how things work. This is an attempt to illustrate the rendering aspect, with what I learned writing pota and reading dom-expressions and voby.

For the Signals library, we are going to use flimsy, which is a small and educative library that implements and documents the reactive core of Solid. Flimsy is around 212~ LOC, code worth a read. The renderer we are going to implement, let's call it clumsy, is around 100~ LOC.

The explorations will get progressively more complicated revisiting and updating what we already wrote. While this doesn't cover everything, it's an initial kick for further articles.

# insert

We need a function with the purpose to insert Nodes in a parent.

This function will be used whenever we have a proper Node.

# create

We also need a function to create nodes from any kind of data. The created nodes are inserted in the document with insert

That's a bit too simplistic, let's make it more fancy by adding more data-types

  1. Avoiding null and undefined by returning early
  2. Allowing Nodes by inserting them directly
  3. Allowing functions, by calling the function and recursing with the returned value

Note how we had to add return statements to avoid the other conditions.

# Placeholders

Now let's add support for Promises, by checking if then it's in the child, when child it's anobject.

Output should have been 123 instead of 132!

As the promise is async and fulfills at a later point in time, our child will be created at an unexpected position in the document.

To solve this problem, we can create an invisible placeholder, add it to the document right away, and hold it to use as a marker for the place on which the children should have been inserted once the promise fulfills.

We will have first to update our insert function to allow inserting nodes at a relative position (using placeholders), instead of just appending to the parent.

Insert the child before the placeholder, and return the added child

Now we can define a function to create placeholders.

The placeholder is created, immediately inserted and returned for use

Then we need to update the code that inserts the promise to use a placeholder

Putting everything together, it should just work.

Everything on its place, order is now 123 as expected

However, we are using a visible span to illustrate how a placeholder works. We can use a comment or an empty text node instead, to hide it from the screen.

For debugging, a comment it's useful because it's visible on the developer tools and can carry debugging text. Once we are done, we can switch to an empty text node to make it invisible in the developer tools.

Typical placeholders

# Signals and Reactivity

Now that we know how to render functions, and keep stuff in position in the document we can introduce Signals

A Signal is a function that holds a value that may or may not change over time. If you are rendering the result of a signal and the value updates, you want to render it in the same position it was, just like we did with the promises.

We learned already how to render functions, and readSignal is a function, so it will render with the code we wrote

However, it's rendering but not updating its value when we click the button. Let's take a look to the code we use to render a function

We will need to use an effect, so when the value of the signal changes, what has been rendered updates too. Fine-Grained updates.

An effect it's a function that tracks signals reads, and re-runs but only when the signals change.

In this case, child is the signal that we want to track

A renderEffect it's just like an effect, but immediately executes. Reactive systems may choose to hold the execution of regular effects, to collect first, useful information that improves the system performance.

The order on which effects execute is not warranted by the reactive system, again, to improve its performance.

By immediately executing with a renderEffect, we ensure the correct order of execution, allowing us to render the document in the order that we expect.

Testing our newly added renderEffect, now the signal change should be updating the document

The signal change it's triggering an update, but we are not removing the old node.

To remove the old node, we can use a cleanup function, which runs when the tracking scope its disposed.

An effect/renderEffect creates a tracking scope, that keeps tracks of which signals have been read. When any of the signals read change, the reactive scope is invalidated, cleanup callbacks run, and the effect function is re-executed

We can ensure the old node is removed by adding cleanup on our insert function. That way, when the tracking scope is invalidated, our node is removed from the document.

Now with the added cleanup it should work as expected

Well, not that fast, the number is moving to the end of the document, a problem similar to the promises we saw before. Placeholders to the rescue.

The signal will keep its position by inserting relative to the placeholder.

Let's try again to see

The placeholder keeps the signal in position

We wrote two of the common internal functions, insert and create, while considering their position in the document with placeholders.

# render

The render function, it's the one that creates a detached tracking scope (a root that doesn't dispose when the parent scope disposes), to hold all nodes created under it. It can be disposed, by calling the function that returns, unmounting all the nodes we created with it.

# Source Code

The code used of this article it's on github. For a real world reactive renderer implementation, you can take a look at pota, voby and dom-expressions.

If you enjoy this topic you can join SolidJS Discord to chat!

Clumsy, partner in mischief with flimsy