It seems like every now and then there’s a new js framework that rises to take the world by storm. Whether it’s jQuery, angularjs, or react, every js developer has that fond memory of the first js framework that got them into the world wide web world.
Nowadays, it seems that all the hype is going towards solid. On the surface, solid looks similar to react, but its core mechanics are different. These differences make Solid incredibly fast and, in my opinion, less prone to some of the errors that react developers often stumble upon in their apps.
So I decided to take the plunge and check out the no virtual dom hype.
The solid in 100 seconds video is quite neat, but I felt I needed some more, so I read this article1 about intro to solid for react developers I watched the advanced intro.
First of all, my mind was blown: everything feels a lot more intuitive, and more importantly, reactive.
A lot of the code I’m used to from react simply ceased to be, things like useCallback
and useRef
.
This is caused by solid’s reactivity. But wait, what’s reactivity?
Reactivity
Well, in order to explain reactivity, first we need to remember how js usually works. When we write something like:
let price = 5
let quantity = 2
let total = quantity * price
console.log(total)
We expect that 10 will be printed in the console. But if we change something, perhaps quantity
, we don’t expect that total
would change:
let price = 5
let quantity = 2
let total = quantity * price
console.log(total)
quantity = 3
console.log(total)
Total should still be 10. But, we can make this code reactive - which means that when we change quantity
or price
, total
will change automatically.
This sort of magic comes to life with the help of the virtual dom in react. When we use hooks, we are literally hooking into the react virtual dom lifecycle:
But when we want to do the same in solid, we notice a few changes:
There’s no dependency array in createEffect
- solid already knows to re run this when price
or quantity
changes!2
In fact, we can move the effect and the state outside the component:
const [count, setCount] = createSignal(0);
createEffect(() => {
document.title = `The current count is: ${count()}`;
});
function Counter() {
return (
<div>
<p>The current count is: {count()}</p>
<button onClick={() => setCount((x) => x + 1)}>Plus</button>
</div>
);
}
We can do this because solid’s reactive primitives themselves, and not the components, are what solid is built on and where the “magic” actually happens. 🤯
In fact, solid proudly describes its components as “vanishing components” - they are only run once, and then they disappear (this is also why you can’t do early returns in solid components).
This is why, in solid, components are helpful for code organization and cease to exist once the initial render occurs.
Reactivity, Round 2
But how exactly is this implemented? Well, solid has quite an extensive repo on github, but in their advanced intro (a.k.a solidjs in 10 minutes) they explain a simplified version of their implementation of reactivity in a way that I found to be truly beautiful and mesmerizing. 🤯
It all starts with a simple state, similiar to react’s own useState
. Let’s call this createSignal
, and implement a basic read and write:
export function createSignal(value) {
const read = () => {
return value;
}
const write = (nextValue) => {
value = nextValue
}
return [read, write]
}
export function createEffect(fn) {
fn();
}
Cool - this indeed gives us a basic ability to read and write to a value, but it is not reactive; an effect won’t be re-run if we change a value!
In order for reactivity to work we need a way to keep track of any effect that observes this signal.
To do that, let’s maintain a global stack called context
which let us to keep track of the effect that is currently running, and a function to get the current effect we’re running in:
const context = [];
function getCurrentEffect() {
return context[context.length - 1]
}
That’s almost ready.
All that’s left is to update this context, so we modify our createEffect
function:
export function createEffect(fn) {
const execute = () => {
context.push(execute);
fn();
context.pop();
}
execute();
}
This makes sure that the effect is pushing itself to the context for the state - so the state could tell it to update when it’s changed. For that to work we also need to modify our createSignal
from before:
export function createSignal(value) {
const subscribers = new Set();
const read = () => {
const current = getCurrentEffect();
if (current) subscribers.add(current)
return value;
}
const write = (nextValue) => {
value = nextValue
for (const s of subscribers) {
s();
}
}
return [read, write]
}
So now when we call write
, it tells each subscriber (each effect) to re-run.
The code now looks like this:
Of course, there are quite a few holes in this basic implementation, including what would happen if the effect fails, or that there’s no batching of updates, but for our basic purposes this suffices.
I hope that this simple exercise of implementing reactivity in js excited you as it blew my mind - I mean, look at how by just using the old plain stack we managed to inject reactivity in our code. 🤯
-
This article was inspired in part by this excellent post ↩
-
Albeit, this comes at a certain price: namely,
state()
is now a function - it’s a getter! ↩
Comments