Learn by building.
Series, not snippets.

Multi-part tutorials with embedded runnable code, sticky outlines, and progress that saves as you scroll. Stop bookmarking scattered posts — start finishing chapters.

In Progress · 8 Chapters · 4h 20m

Building a Reactive UI Framework from Scratch

From signals to a working renderer — eight chapters, zero dependencies, one tiny framework you'll actually understand.

M
J
S
+2,847 builders enrolled
Your progress 2 / 8 chapters
signal.js
const [count, setCount] =
  signal(0)

effect(() => {
  log(`count: ${count()}`)
})

setCount(1)
setCount(2)
// → count: 1
// → count: 2

Every series, one place.

Pick a path. Track your progress. Finish what you start.

Library Reactive Frameworks

Building a Reactive UI Framework from Scratch

Most "reactivity" tutorials stop at useState. This one doesn't. Over eight chapters you'll build a working signal graph, a virtual DOM, a diffing algorithm, and a component model — then ship a 4kb framework you fully own.

M
Maya Okonkwo
Author · Framework Eng @ Plot
8 chapters 4h 20m Intermediate
Continue Chapter 3
Series Progress 25%
Chapters completed 2 / 8
Time invested 1h 12m
Estimated remaining 3h 08m
Last visited 2 days ago
Next up
3
The Reactive Signal: Tracking Dependencies
32 min · 4 code playgrounds

Chapters

2 of 8 complete
Completed · 22 min
Foundations: What We're Building
Set up the project, define the API surface, and write our first test.
Completed · 28 min
The Signal Primitive
A getter/setter pair that knows when it's been read.
3
In Progress · 32 min · 4 playgrounds
Tracking Dependencies Automatically
Effects that subscribe themselves. The magic of a running-effect stack.
4
Available · 38 min
The Virtual DOM, Demystified
Why we diff trees instead of mutating nodes — and what costs we trade.
Requires Ch 3, 4
The Rendering Pipeline
Wiring signals to the DOM without infinite loops.
Requires Ch 5
Components & Composition
Reusable units with their own state and lifecycle.
Requires Ch 6
State Management & Stores
Coordinating signals across component boundaries.
Requires Ch 5, 7
Putting It Together: Ship It
Bundle, tree-shake, and publish your 4kb framework.

Prerequisite map

Hover a node
1 Foundations 2 Signal 3 Tracking 4 Virtual DOM 5 Rendering 6 Components 7 Stores 8 Ship It
Done Current Open Locked
Hover a chapter to trace its dependency chain.
Reactive Frameworks Chapter 3 of 8

The Reactive Signal: Tracking Dependencies Automatically

In Chapter 2 we built a signal that knows when it's been read. Now we'll teach effects to subscribe themselves — no manual wiring, no forgotten subscriptions, no stale renders.

32 min read 4 playgrounds 1 diff widget Updated Mar 14

In Chapter 2 we built a primitive that could hold state and announce when it changed. That was the easy half. The hard half — the half that every reactive framework has to solve — is figuring out which effects care about which signals, without making the developer hand-wire every subscription.

This is the chapter where "reactive" stops being a buzzword and starts being automatic. By the end you'll have a thirty-line module that powers everything else in the framework.

If you skipped straight here, that's fine — but you'll want the createSignal from Chapter 2 in your head. We're building directly on top of it.

01The problem with manual subscriptions

Here's the naive version of reactivity — and most early frameworks shipped exactly this:

naive.js
const count = createSignal(0)

// manually subscribe an effect
count.subscribe(() => {
  document.body.textContent = `Count is ${count()}`
})

count.set(1) // → "Count is 1"

That works. For one signal. For one effect. And only as long as you remember to call subscribe in exactly the right place.

The moment you compose three signals into a derived value, the model breaks down. Which signal should you subscribe to — the derived one, or the underlying primitives? Both? What if the derived computation is expensive — do you re-run it on every change, or memoize?

The honest answer: you shouldn't have to think about this at all. The framework should observe what your effect reads and wire up subscriptions automatically.

02A running-effect stack

The trick is older than JavaScript: maintain a stack of currently-executing effects. When a signal is read, it checks "is there an effect running right now?" — and if so, registers itself as a dependency of that effect.

Three pieces, in order:

  • A stack — a list of effects currently executing. Push on entry, pop on exit.
  • A read hook — every signal's getter peeks at the stack and adds itself to whichever effect is on top.
  • A cleanup pass — before re-running an effect, unsubscribe it from all its previous dependencies. Otherwise, branching reads accumulate stale subscriptions.

That's the whole mechanism. Everything else — batching, scheduling, memoization — is an optimization on top.

Let's see it working. Edit the code below and watch the output update as you type.

03Hands-on: build your own signal

This playground is live. Edit the JavaScript and the output panel updates on every keystroke. Try changing the initial value, adding a new signal, or composing two signals together.

Live Playground
signal-tracker.js
Live output

If everything's wired up, clicking Increment bumps the count and the DOM updates without you ever writing addEventListener or calling render(). The effect re-ran because it read count() during its first execution — that read registered it as a dependent.

Try this: rename the variable and watch the effect not re-run when name changes but count doesn't appear in the effect body. The dependency graph is precise, not coarse.

04Before & after: auto-tracking

Here's the same logic, written two ways. The "before" version is what most tutorials teach. The "after" version is what production frameworks actually ship. Click the toggle to watch the transition.

Manual vs. automatic subscriptions
Click to apply the reactive upgrade.
// BEFORE — every subscription is manual

const count = createSignal(0);

// developer must remember to subscribe
count.subscribe(() => {
  document.body.textContent = `Count: ${count()}`;
});

// adding a derived value means more wiring
const doubled = createSignal(0);
count.subscribe(() => doubled.set(count() * 2));

// and another subscription for the UI
doubled.subscribe(() => {
  document.title = `Doubled: ${doubled()}`;
});

count.set(5);
// → body updates, doubled updates, title updates
// but you wrote 3 subscribe calls. scale that to 50 signals.
// AFTER — effects auto-track their dependencies

const [count, setCount] = createSignal(0);

// the framework figures out what to subscribe to
createEffect(() => {
  document.body.textContent = `Count: ${count()}`;
});

// derived values compose without wiring
createEffect(() => {
  document.title = `Doubled: ${count() * 2}`;
});

setCount(5);
// → both effects re-run automatically
// dependency tracking is precise: only what you read,
// only when it actually changed. zero subscribe() calls.

The behavioral difference is subtle but profound. The "after" version isn't just shorter — it's correct by construction. There's no way to forget a subscription, no way to subscribe to the wrong signal, no way to leak a subscription you no longer need.

05Common pitfalls

Now that you've seen the mechanism, here are the three ways it bites back:

1. Reading a signal outside an effect

If you call count() in event handler code that runs outside an effect, no subscription is registered. That's correct — but it confuses people who expect "any read anywhere" to be reactive. Reads are only tracked inside effects.

2. Conditional reads

If your effect reads a() on the first run and b() on the second, the subscription set changes between runs. The cleanup-before-rerun pattern handles this — but only if you implement it. Skip the cleanup and you've built a memory leak.

3. Synchronous write loops

Writing to a signal inside an effect that reads it creates an infinite loop. Production frameworks add cycle detection; we'll do that in Chapter 5.

Pitfall #2 is why our createEffect clears its dependencies before re-executing. Without that line, branching logic would slowly accumulate stale subscribers until your app ground to a halt.

06Wrap up

You now have, in about thirty lines, the same reactivity model that powers SolidJS, Preact Signals, and Vue's ref/effect pair. The names are different, the optimizations are wilder, but the core idea — a running-effect stack — is identical.

In Chapter 4 we'll leave the signal layer and move to the DOM. We need a way to describe what the UI should look like, then diff that against what's there. That's the virtual DOM — and despite the name, there's nothing virtual about how real it is.

Continue to Chapter 4 when you're ready. Or scroll back to the prerequisite map and trace where this chapter sits in the larger plan.

Chapter 3 — 24 comments

Y
M
Maya Okonkwo Author 2 days ago

Quick note for anyone who got stuck on the cleanup step: the this.deps.clear() call must happen after you've removed yourself from each subscriber set, not before. Otherwise you lose the references you need to unsubscribe from.

T
Theo Nakamura 1 day ago

This was exactly my bug. Spent 20 minutes wondering why my subscribers set kept growing. 🙏

P
Priya Raman 3 days ago

The diff widget is killer. I've read about "automatic dependency tracking" a dozen times and it never clicked until I saw the two versions side by side. More of this please.

J
Jonas Vetter 4 days ago

Question on pitfall #3 — when you say "production frameworks add cycle detection," what does that look like? Is it a depth counter on the effect stack, or something smarter?

S
Sofia Bekova 5 days ago

For anyone curious about the historical version of this pattern, the original MobX paper ("Origin of reactive programming" blog post, 2015) describes essentially the same running-observer stack. Worth a read after this chapter.

Chapter 3 complete!
Chapter 4 is unlocked.