Build a reactive store from scratch using Javascript

vanyaxk

Ivan

Posted on November 3, 2024

Build a reactive store from scratch using Javascript

Reactive programming is a neat approach that allows you to create applications which dynamically reflect data changes. It’s the core tech behind many modern JavaScript frameworks like React and Vue - it updates in response to user actions or other state changes. Understanding what is under the hood of reactivity can feel like too much work, it feels like one of those 'magic' abstractions that the frameworks are for. But what if you could build a small reactive system yourself to see how it really works?

This article will walk through the basics of reactive programming by building a simple reactive store from scratch in JavaScript. We’ll walk through the key concepts, including dependency tracking and automatic updates, in a minimal implementation. By the end, you should be able to understand how to create reactive data structures that automatically track dependencies and trigger updates whenever the state changes. This approach will help you understand reactivity and will give you the tools to experiment with it on your own, and possibly apply it to your projects.

Let's get started by looking at the core components of a reactive system that we are going to use:

  • Effects: Functions that automatically run in response to changes in reactive data. They are registered using the effect function, which tracks dependencies on any accessed signals.
  • Signals: Reactive data properties that notify dependent effects whenever their values change.
  • Dependencies: The relationships between signals and the effects that rely on them. Dependencies are tracked so that changes in signals trigger updates to the relevant effects.

Now that we are past reactive programming definitions, let's also mention the Javascript APIs we are going to use:

Proxy: The Proxy object allows you to create a proxy for another object, enabling you to define custom behavior for fundamental operations (like property access and assignment). In this code, it's used to make the reactive store (the state object) responsive to changes.

Reflect: The Reflect API provides methods for interceptable JavaScript operations. It is used to perform operations like Reflect.get and Reflect.set in the reactive function, which allows the proxy to handle property access and assignment while maintaining the original behavior of the object.

Map: The Map object is a collection that holds key-value pairs, where keys can be any data type. It is used in this implementation to create the dependencyMap, which tracks the dependencies associated with each signal.

Now, let's start defining what our initial state will look like:


// Let's define a Map object to track our dependencies
const dependencyTrackerMap = new Map();
// The activeEffect variable will hold the currently executing 
// effect function. 
// It will be set when an effect is run and will be used
// to track which effects depend on specific reactive properties. 
let activeEffect = null

// This function will make an object reactive
function reactive(target) {
    return new Proxy(target, {
        get(obj, prop) {
            trackDependency(prop); // Track dependency
            return Reflect.get(obj, prop);
        },
        set(obj, prop, value) {
            const result = Reflect.set(obj, prop, value);
            triggerDependency(prop); // Trigger reactions
            return result;
        }
    });
}

// the effect function will register reactive functions
function effect(fn) {
    activeEffect = fn;
    fn(); // Run the function once to register dependencies
    activeEffect = null;
}

// this function will track dependencies
function trackDependency(key) {
    if (activeEffect) {
        if (!dependencyTrackerMap.has(key)) {
            dependencyTrackerMap.set(key, new Set());
        }
        dependencyTrackerMap.get(key).add(activeEffect);
    }
}

// this function will trigger dependencies
function triggerDependency(key) {
    const deps = dependencyTrackerMap.get(key);
    if (deps) {
        deps.forEach(effect => effect());
    }
}

// This will create a reactive object with an initial state
// count and message here are signals
const state = reactive({ count: 0, message: "Hello" });
Enter fullscreen mode Exit fullscreen mode

So, here is what we have done:

  1. We created a reactive function that takes in an object and returns a Proxy which will track what changes will be done to the object in the future
  2. We created an effect function which registers reactive functions that depend on the state. When an effect is defined, it runs immediately to register any dependencies and sets activeEffect accordingly.
  3. We created a dependency tracker which consists of three parts: the trackDependency function checks if there is an active effect when a reactive property is accessed. If yes, it adds that effect to a set of dependencies for the corresponding property in dependencyTrackerMap. In turn, the triggerDependency function retrieves the set of dependent effects associated with a property from dependencyTrackerMap and executes each of them when the property value changes.

Now, let's create an effect with a callback and try to trigger it:

//We are using state from the previous snippet:
effect(() => {
    console.log(Count has changed: ${state.count});
});

effect(() => {
    console.log("Message has changed");
    console.log(The new message is: ${state.message});
});
Enter fullscreen mode Exit fullscreen mode

The console logs will trigger when we try to update the state we have created:

// Modifying state (or updating a signal) triggers the dependent effects:

state.count++; 
// "Count has changed: 1"
state.message = "Hello!!!"; 
// "Message has changed"
// "The new message is: Hello!!!"
Enter fullscreen mode Exit fullscreen mode

Here is a little visualization of what happens when a dependency is triggered:

reactivity example excalidraw diagram


In this article, we explored how to create a basic reactive system in JavaScript, enabling automatic updates (or side effects) in response to changes in data. This implementation serves as an introduction to reactive programming concepts, which is part of the framework 'magic'. Besides, we've learned what Proxy and Reflect APIs do, and used them, as well as the Map object.

In summary, this reactive system manages dependencies and automatically updates effects when the state changes. By registering functions that rely on specific reactive properties, the system tracks which functions depend on which properties, re-running them when needed. This approach allows to create responsive applications where state changes are automatically reflected in the UI without additional code, improving developer experience and making data handling easier and more efficient.

💖 💪 🙅 🚩
vanyaxk
Ivan

Posted on November 3, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related