Reactivity

Introduction

Reactivity is the foundation for making a Solid app interactive. In this concept article, we'll discuss the basics of reactivity and how it works in Solid.

Reactivity is a programming pattern that lets you set up behavior that "reacts" to data changes automatically.

For example, you might make a shopping cart with a label that displays the total cost of the items. With reactivity, you can tell that label to automatically update whenever an item gets added or removed.

This way, you only have to manage the data (like adding an item to the cart). Once you wire up the UI elements (like the label) to the data, the UI updates (changing the label) are handled for you.

To get started in Solid, you need a basic understanding of reactive signals and effects. In this article, we'll teach you how those work behind the scenes. This deeper understanding of reactivity can help you optimize your code, debug issues, and build your own extensions to the reactive system.

Reactive Primitives

To use reactivity, you create reactive primitives. The most important primitives are signals and effects. In the sections below, we'll explain what signals and effects are, and then implement a simple version of them ourselves.

Other reactive primitives

Solid has other reactive primitive, but they are derived from signals and effects. For example:

  • Stores are trees of signals
  • Memos are signals that update like effects
  • Resources are signals that update when data is fetched from a server
  • Render effects are effects that initially run earlier

Signals

A signal represents a piece of data that can change. For example, a signal could be a username or the value of a counter that we can increment. Signals consist of two main parts:

  • The getter, a function that lets you access the current value of the signal
  • The setter, a function that lets you set the current value of the signal.

We create a signal using Solid's createSignal function. This returns the getter and and setter as a two-element array. We typically use array destructuring to assign the getter and setter into variables with names of our choosing.

jsx
import { createSignal } from "solid-js";
const [count, setCount] = createSignal(1);
console.log(count()); // prints "1"
setCount(0); // Sets the count value to 0
jsx
import { createSignal } from "solid-js";
const [count, setCount] = createSignal(1);
console.log(count()); // prints "1"
setCount(0); // Sets the count value to 0

In this example, count is the getter and setCount is the setter.

Effects

An effect represents the action we would like to take when the data in one or more signals change. Solid provides a createEffect function that itself takes in a function. Solid will run that function, and then rerun it whenever any signal inside that function changes value.

For example, the following effect will console.log the count whenever it updates:

jsx
import { createSignal, createEffect } from "solid-js";
const [count, setCount] = createSignal(0);
createEffect(() => {
console.log(count());
});
jsx
import { createSignal, createEffect } from "solid-js";
const [count, setCount] = createSignal(0);
createEffect(() => {
console.log(count());
});

Creating a reactive system

One of the best ways to understand how reactivity works is to implement it ourselves. In this section, we will implement the same reactive pattern Solid uses: the observer pattern. In the observer pattern, data (signals) maintain a list of their subscribers (effects). When its data changes, a signal triggers all of its subscribers.

Let's use the same names, createSignal and createEffect, for our implementation:

jsx
function createSignal() {}
function createEffect() {}
const [count, setCount] = createSignal(0);
createEffect(() => {
console.log("The count is " + count());
});
jsx
function createSignal() {}
function createEffect() {}
const [count, setCount] = createSignal(0);
createEffect(() => {
console.log("The count is " + count());
});

First, let's handle the basics of the createSignal function. It needs to:

  • initialize the count value to 0 (the argument provided to createSignal)
  • return a two-element array consisting of a getter and a setter function
jsx
function createSignal(initialValue) {
let value = initialValue;
function getter() {
return value;
}
function setter(newValue) {
value = newValue;
}
return [getter, setter];
}
jsx
function createSignal(initialValue) {
let value = initialValue;
function getter() {
return value;
}
function setter(newValue) {
value = newValue;
}
return [getter, setter];
}

We can now get the current value of our signal by calling the getter and we can set the value by using the setter. This is great, but there is no reactivity yet.

Next, let's set up createEffect. We know it takes a function and runs it:

jsx
function createEffect(fn) {
fn();
}
jsx
function createEffect(fn) {
fn();
}

The key to reactivity is establishing the relationship between createSignal and createEffect.

To do this, we give each signal a subscriber list. When we give createEffect a function and run it, we want a way to tell any signals that are called along the way to add that function to their subscriber list. Our next steps:

  • Create a global currentSubscriber that can keep track of the function we pass to createEffect
  • Register the function we pass to createEffect as the current subscriber
  • When we access a signal, add the current listener to a list of subscribers
  • When we set the signal to a new value, run all subscribers
jsx
let currentSubscriber = null;
function createSignal(initialValue) {
let value = initialValue;
// Maintain a list of a signal's own subscribers
const subscribers = new Set();
function getter() {
// Add to subscriber list
if (currentSubscriber) {
subscribers.add(currentSubscriber);
}
return value;
}
function setter(newValue) {
value = newValue;
// Notify all subscribers of the value change
for (const subscriber of subscribers) {
subscriber();
}
}
return [getter, setter];
}
function createEffect(fn) {
// Add function as subscriber in global scope
currentSubscriber = fn;
// Run function to trigger the accessor of any signals that are called
fn();
// Remove the function as the current subscriber
currentSubscriber = null;
}
jsx
let currentSubscriber = null;
function createSignal(initialValue) {
let value = initialValue;
// Maintain a list of a signal's own subscribers
const subscribers = new Set();
function getter() {
// Add to subscriber list
if (currentSubscriber) {
subscribers.add(currentSubscriber);
}
return value;
}
function setter(newValue) {
value = newValue;
// Notify all subscribers of the value change
for (const subscriber of subscribers) {
subscriber();
}
}
return [getter, setter];
}
function createEffect(fn) {
// Add function as subscriber in global scope
currentSubscriber = fn;
// Run function to trigger the accessor of any signals that are called
fn();
// Remove the function as the current subscriber
currentSubscriber = null;
}

This is all the code needed to create a basic reactive system. We can demonstrate that it works by incrementing the count value every second and watching the console.

jsx
const [count, setCount] = createSignal(0);
createEffect(() => {
console.log("The count is " + count());
});
setInterval(() => {
setCount(count() + 1);
}, 1000);
jsx
const [count, setCount] = createSignal(0);
createEffect(() => {
console.log("The count is " + count());
});
setInterval(() => {
setCount(count() + 1);
}, 1000);
Incrementing counter

We have just implemented the most basic form of Solid's reactivity system!

Effect tracking is synchronous

One observation we should note about our reactivity system is that it's synchronous. It registers the subscriber globally, runs the effect, and unregisters the subscriber.

So what happens if our createEffect function looks like this?

jsx
createEffect(() => {
setTimeout(() => {
console.log(count());
}, 1000);
});
jsx
createEffect(() => {
setTimeout(() => {
console.log(count());
}, 1000);
});

Our createEffect implementation doesn't wait around for this setTimeout callback to execute, so by the time we call our count getter, there is no subscriber in the global scope. The count signal won't register this callback as one of its subscribers.

Handling asynchronous effects

Solid gives you some options for handling asynchronous effects. For example, you can use the on function [TODO: add API link] to manually specify effect dependencies.

To learn more about Solid-specific tracking mechanisms, see the Tracking Concept documentation.

Learning more

We hope you enjoyed this introduction to reactivity! If you would like to dive deeper, please check out the following resources: