Build a Reactivity Engine
Interactive lab

55 min

Computed Signals

Add lazy, cached createComputed that invalidates when dependencies change.

~/playground — 05-computed-signals · edit engine.js

Live
let activeEffect = null
const effectCleanups = new WeakMap()
const disposed = new WeakSet()
let batchDepth = 0
const queuedEffects = new Set()
let flushScheduled = false

function flushEffects() {
  flushScheduled = false
  const toRun = [...queuedEffects]
  queuedEffects.clear()
  toRun.forEach((fn) => {
    if (!disposed.has(fn)) fn()
  })
}

function scheduleEffect(fn) {
  if (disposed.has(fn)) return
  queuedEffects.add(fn)
  if (batchDepth > 0) return
  if (!flushScheduled) {
    flushScheduled = true
    queueMicrotask(flushEffects)
  }
}

function runEffect(effect, fn) {
  const prevCleanup = effectCleanups.get(effect)
  if (prevCleanup) prevCleanup()
  activeEffect = effect
  const cleanup = fn()
  activeEffect = null
  if (typeof cleanup === 'function') effectCleanups.set(effect, cleanup)
  else effectCleanups.delete(effect)
}

export function createSignal(initial) {
  let value = initial
  const subscribers = new Set()
  return {
    get() {
      if (activeEffect && !disposed.has(activeEffect)) subscribers.add(activeEffect)
      return value
    },
    set(next) {
      value = next
      subscribers.forEach((fn) => scheduleEffect(fn))
    },
    subscribe(fn) {
      subscribers.add(fn)
      return () => subscribers.delete(fn)
    },
  }
}

export function batch(fn) {
  batchDepth++
  try {
    return fn()
  } finally {
    batchDepth--
    if (batchDepth === 0 && queuedEffects.size && !flushScheduled) {
      flushScheduled = true
      queueMicrotask(flushEffects)
    }
  }
}

export function createEffect(fn) {
  const effect = () => {
    if (disposed.has(effect)) return
    runEffect(effect, fn)
  }
  effect()
  return effect
}

export function disposeEffect(effect) {
  if (disposed.has(effect)) return
  disposed.add(effect)
  queuedEffects.delete(effect)
  const cleanup = effectCleanups.get(effect)
  if (cleanup) cleanup()
  effectCleanups.delete(effect)
}


export function createComputed(fn) {
  // TODO: lazy cached computed with invalidation
  throw new Error('Not implemented')
}

Preview goal: This lab is test-driven — verify caching and invalidation with Run tests.

Checkpoints

  • Computed caches until dependency changes
  • All Sandpack tests pass