Multi-part tutorials with embedded runnable code, sticky outlines, and progress that saves as you scroll. Stop bookmarking scattered posts — start finishing chapters.
From signals to a working renderer — eight chapters, zero dependencies, one tiny framework you'll actually understand.
const [count, setCount] = signal(0) effect(() => { log(`count: ${count()}`) }) setCount(1) setCount(2) // → count: 1 // → count: 2
Why we diff trees instead of mutating nodes — and what costs we trade for it.
Calling Rust from JavaScript without losing your mind (or your strings).
OPFS-backed persistence with batched transactions — and zero IndexedDB trauma.
WebCrypto basics, key management patterns, and where crypto actually belongs.
Pick a path. Track your progress. Finish what you start.
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.
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.
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.
createSignal from Chapter 2 in your head. We're building directly on top of it.
Here's the naive version of reactivity — and most early frameworks shipped exactly this:
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.
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:
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.
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.
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.
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.
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.
Now that you've seen the mechanism, here are the three ways it bites back:
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.
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.
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.
createEffect clears its dependencies before re-executing. Without that line, branching logic would slowly accumulate stale subscribers until your app ground to a halt.
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
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.This was exactly my bug. Spent 20 minutes wondering why my subscribers set kept growing. 🙏
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.
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?
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.