Modern Web

Building a Custom Reactive State Manager in 50 Lines of Vanilla JS

Learn how modern reactive systems like SolidJS or Svelte Runes work. Create a lightweight, high-performance Signal reactive state manager in pure JS.

Sachin Sharma
Sachin SharmaCreator
May 31, 2026
4 min read
Building a Custom Reactive State Manager in 50 Lines of Vanilla JS
Featured Resource
Quick Overview

Learn how modern reactive systems like SolidJS or Svelte Runes work. Create a lightweight, high-performance Signal reactive state manager in pure JS.

Building a Custom Reactive State Manager in 50 Lines of Vanilla JS

Reactivity is the heartbeat of modern web development. Whether you write Svelte Runes, SolidJS, Vue Composition API, or Preact Signals, the underlying paradigm is identical: when state changes, dependent computations or UI elements must automatically update.

While libraries abstract this magic away behind beautiful APIs, understanding how reactivity works under the hood is a superpower. It transforms state management from a black box of magic into a deterministic engineering pattern.

In this guide, we will demystify reactivity by building a fully functioning, high-performance, lightweight Signals/Reactive state manager in under 50 lines of pure, vanilla Javascript using the modern Proxy API.


⚑ 1. The Reactivity Concept: Prover & Subscriber

At its core, a reactive system requires three components:

  1. 2.
    A Signal (State): A wrapper containing a value. When read, it records who read it. When written to, it notifies all readers.
  2. 4.
    An Effect (Subscriber): A wrapper surrounding a function. When executed, it collects any Signals read during its execution.
  3. 6.
    A Global Stack: A tracker keeping track of which Effect is currently running so that Signals know who is subscribing to them.

πŸ—οΈ 2. The Implementation

Let's write our micro-reactivity library. Create a file named reactive.js:

javascript
// 1. Maintain a global stack of currently executing effects const contextStack = []; // 2. Define our Signal constructor export function signal(initialValue) { let value = initialValue; // Maintain a unique set of subscribers (effects) const subscribers = new Set(); return { // Getter: collect the active subscriber get value() { const runningEffect = contextStack[contextStack.length - 1]; if (runningEffect) { subscribers.add(runningEffect); } return value; }, // Setter: notify all subscribers when value changes set value(newValue) { if (value !== newValue) { value = newValue; // Trigger all registered subscribers for (const effect of subscribers) { effect(); } } } }; } // 3. Define our Effect runner export function effect(fn) { const execute = () => { contextStack.push(execute); try { fn(); // Run actual computation, triggering getters } finally { contextStack.pop(); // Clean up stack } }; execute(); // Run immediately to establish initial subscriptions }

πŸ› οΈ 3. Using Our Vanilla Reactive Manager

Let's see this in action by building a dynamic billing calculator:

javascript
import { signal, effect } from "./reactive.js"; // Initialize our reactive signals const price = signal(100); const quantity = signal(2); const taxRate = signal(0.18); let totalBilling = 0; // Establish a reactive effect effect(() => { // Reading these getters automatically registers this effect as a subscriber const subtotal = price.value * quantity.value; totalBilling = subtotal + (subtotal * taxRate.value); console.log(`[Reactive Update] Total Billing is now: $${totalBilling}`); }); // Output: [Reactive Update] Total Billing is now: $236 // Mutating a signal automatically updates our totalBilling! quantity.value = 5; // Output: [Reactive Update] Total Billing is now: $590 price.value = 80; // Output: [Reactive Update] Total Billing is now: $472

πŸš€ 4. Scaling Up: The Proxy-Based Store

While Signals are incredible for singular primitive values, managing complex objects and nested state gets verbose. We can leverage Javascript's native Proxy API to build a reactive object store:

javascript
export function store(initialObject) { const subscribers = new Set(); return new Proxy(initialObject, { get(target, prop, receiver) { const runningEffect = contextStack[contextStack.length - 1]; if (runningEffect) { subscribers.add(runningEffect); } return Reflect.get(target, prop, receiver); }, set(target, prop, value, receiver) { const oldValue = target[prop]; if (oldValue !== value) { Reflect.set(target, prop, value, receiver); // Trigger updates for (const effect of subscribers) { effect(); } } return true; } }); }

Here is how you use the Proxy store:

javascript
const userSession = store({ username: "Sachin", isLoggedIn: false }); effect(() => { console.log(`UI State: User ${userSession.username} is logged in: ${userSession.isLoggedIn}`); }); // Mutating dynamic properties instantly updates subscribers! userSession.isLoggedIn = true; // Output: UI State: User Sachin is logged in: true

🏁 5. Conclusion: Reactivity is Simple

Modern frameworks bundle massive routers, compilers, and virtual DOM systems, which can make reactivity feel like incomprehensible magic. Under the hood, however, it remains a elegant implementation of the classic Observer pattern, beautifully supercharged by modern JS features like the contextStack and Proxy traps.

By building a micro-reactivity library, you learn to write cleaner state logic, optimize rendering performance, and understand how modern frameworks operate at their core.

Sachin Sharma

Sachin Sharma

Software Developer

Building digital experiences at the intersection of design and code. Sharing weekly insights on engineering, productivity, and the future of tech.