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.

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:
- 2.A Signal (State): A wrapper containing a value. When read, it records who read it. When written to, it notifies all readers.
- 4.An Effect (Subscriber): A wrapper surrounding a function. When executed, it collects any Signals read during its execution.
- 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:
javascriptimport { 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:
javascriptexport 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:
javascriptconst 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.

Bun 1.2 vs. Node.js 22 vs. Deno 2.0: The Ultimate 2026 HTTP Throughput & Memory Benchmark
A rigorous, standardized developer-focused comparison of the three primary JavaScript runtimes of 2026, measuring raw throughput, memory leaks, and package manager overhead.

Postgres Row Level Security (RLS): Building Multi-tenant SaaS Backends Safely
Ditch manual tenant filters. Learn how to secure multi-tenant SaaS applications at the database level using Postgres Row Level Security (RLS) policies.