Skip to content

A brief history of Signals

The signal pattern is widely associated with Angular’s 2023 adoption and Preact’s 2022 release, but the underlying idea predates both by more than a decade. This page traces the lineage honestly — what each contributor brought, what didn’t survive contact with real apps, and where Phaze fits in the chain.

yearlibraryshipped
2010Knockout.jsko.observable() + ko.computed()
2014S.jsPure synchronous signal primitive
2015MobXobservable, computed, autorun
2016Vue 2Reactive data() properties
2018SolidJSModern “signal” terminology + JSX integration
2020Vue 3ref(), computed(), reactive() (Composition API)
Sep 2022@preact/signalssignal() with .value accessor
May 2023Angular Signalssignal(), computed(), effect() (Angular 16)
2024Svelte 5 (runes)$state, $derived, $effect (compiler-emitted)
2024+TC39 Signals proposalCross-framework standardization (Stage 1)

The original. Released July 5, 2010. The first mainstream JS implementation of auto-tracking observables.

What it brought

  • ko.observable(value) — a reactive cell with () getter / (v) setter.
  • ko.computed(fn) — derived value that auto-tracks its dependencies.
  • The auto-dependency-tracking algorithm itself — read inside a computed, get subscribed.
  • MVVM data binding via data-bind="text: name" HTML attributes.

What didn’t survive

  • The MVVM pattern as a dominant shape (component-based UI ate it).
  • HTML-attribute data binding (data-bind="...") — JSX won.
  • The library itself is no longer fashionable, though it still ships and works.

What Phaze carries forward The auto-tracking algorithm. Knockout’s “read inside a computation, get subscribed” is exactly what Phaze’s track() / notify() runtime does. That algorithm hasn’t fundamentally changed in 15 years.


The cleanest pure-signal implementation of its era. The design that Solid would later build on.

What it brought

  • S.data(initial) — writable signal.
  • S(fn) — computation (auto-tracked, like a computed).
  • Synchronous semantics — writes propagate immediately, before the next line runs. (Most others are async-microtask.)
  • The “everything is a function” reading API — count() not count.value.

What didn’t survive

  • S.js as a UI framework on its own (it wasn’t really one — just the primitives).
  • Strict synchronous propagation — Solid switched to batched + microtask-flushed because synchronous storms are hard to tame in real apps.

What Phaze carries forward The function-call read API (count()). Solid kept it, Angular kept it, Phaze keeps it. Preact diverged with .value.


The first library to scale signals to large React apps. Showed that fine-grained reactivity wasn’t just a curiosity.

What it brought

  • observable(obj) — automatically wraps an object so property reads track and writes notify.
  • autorun(fn) — runs once, tracks deps, re-runs on change. (The shape effect() later re-took.)
  • Action-based mutations — wraps writes in runInAction to batch.
  • The observable / computed / autorun triad — the same triad every signals library has since.

What didn’t survive

  • Decorator API (@observable, @computed) — the JS decorator proposal stalled for years; everyone moved to function APIs.
  • Class-heavy state — MobX paired naturally with class components; functional components killed that.
  • Transparent deep proxying as a default — too magical for many. Came back partially as opt-in (Vue’s reactive(), Phaze’s phaze/store).

What Phaze carries forward The triad shape: signalobservable, computedcomputed, effectautorun. Same primitives, smaller surface. Plus Phaze’s optional phaze/store resurrects the deep-proxy idea as an opt-in.


Mainstream legitimation. Vue 2 tied reactivity to the component instance; Vue 3 broke that and made the primitives standalone.

What Vue 2 brought

  • Reactive data() properties via Object.defineProperty.
  • The computed: block in components.

What Vue 3 brought

  • ref(value) — standalone signal-shaped reactive ref. (.value accessor — Preact later copied this.)
  • computed(fn) — same primitive as everyone else’s.
  • reactive(obj) — Proxy-wrapped deep object reactivity (revival of MobX’s transparent model).
  • The Composition API — primitives usable outside component scope.

What didn’t survive (Vue 2 → Vue 3)

  • Object.defineProperty (replaced by Proxy in Vue 3 — handles new keys, arrays, etc.).
  • Component-instance coupling. Vue 3 made ref / computed standalone.
  • The Options API for new code (still supported, but Composition API is recommended).

What Phaze carries forward The phaze/store deep proxy is conceptually the same as Vue’s reactive(obj). Both wrap plain objects so reads track per-property and writes notify per-property. Phaze ships it as opt-in subpath rather than tied to the component model.


The pivot. Took the auto-tracking pattern and put it inside a modern JSX-based UI framework with a compiler.

What it brought

  • The term “signal” in modern JS. (createSignal, createMemo, createEffect — though the term itself echoes much earlier reactive programming literature.)
  • Compiler-emitted fine-grained DOM bindings — no virtual DOM, no diffing, no re-render. Each binding subscribes to exactly the signals it reads.
  • The tuple shape: const [count, setCount] = createSignal(0).
  • <For>, <Show>, <Switch>, <Match>, <Dynamic> — the named flow components that Phaze has since trimmed.
  • createResource for async data.
  • <ErrorBoundary> (Solid’s name; Phaze’s <Catch>).

What didn’t survive

  • The [get, set] tuple shape didn’t propagate. Preact, Angular, and the TC39 proposal all use a single signal value with methods.
  • <Show> and friends — convenient but redundant once compiler+ternary works (Phaze deletes them).
  • The full Solid surface — Phaze ships ~30% of Solid’s named exports.

What Phaze carries forward The compiler approach (Phaze ships @madenowhere/phaze-compile). The fine-grained DOM bindings — Phaze emits the same shape. The <For> keyed reconciliation. The error boundary as a runtime hook (renamed <Catch>). The control-flow-via-computed philosophy.


The vendor stamp. Brought signals to the Preact/React-flavored audience without changing the host runtime.

What it brought

  • signal(), computed(), effect() exported from a separate package.
  • The .value accessor (read and write via count.value). A divergence from Solid’s count() and Angular’s later count().
  • Compatibility with React-style top-down render — signals notify a virtual-DOM micro-update, not a full re-render.
  • Marketing: signals visible to ~5M+ Preact/React developers.

What didn’t survive (or didn’t propagate)

  • The .value accessor — not adopted by Solid, Angular, or TC39. The function-call shape won.
  • Shipping signals as a separate package (@preact/signals not preact) — Phaze ships them in core.
  • Crediting prior art — the announcement post doesn’t mention SolidJS, S.js, MobX, or Knockout.js. This isn’t a moral failing, but it’s worth noting that the “where did this come from?” question wasn’t answered there.

What Phaze carries forward Almost nothing of the syntax — Phaze uses Solid-style count() reads. But Preact’s contribution to the narrative (signals are mainstream, not exotic) was real, and Phaze benefits from the audience Preact opened up.


Late but consequential. Brought the pattern into Google’s enterprise framework.

What it brought

  • signal(), computed(), effect() — function-call API matching Solid.
  • update(fn) method for transformations.
  • Integration with Angular’s existing change-detection model (zone.js coexistence — a difficult marriage).
  • Massive user base exposed to the pattern overnight.

What didn’t survive (still in flux)

  • The zone.js coexistence story is the main open question. Angular is gradually moving toward “zoneless” rendering driven entirely by signals — that migration is incomplete.
  • Several intermediate APIs from Angular’s signal preview have been retired or renamed.

What Phaze carries forward The function-call API (already from Solid). The update(fn) method — Phaze has it too. The signal() / computed() / effect() triad. Phaze doesn’t have Angular’s zone problem because Phaze has no zone — it’s reactive from the bottom up.


Compiler-only signals. The same pattern, but the compiler emits all the wiring; users see $state like a magic variable.

What it brought

  • $state(initial) — looks like assignment, behaves like a signal.
  • $derived(fn) — computed.
  • $effect(fn) — effect.
  • All of these are compiler-emitted; the runtime is hidden.

What didn’t survive (yet — too early)

  • Whether the $rune syntactic magic catches on outside Svelte. Other frameworks haven’t picked up $state as a primitive shape.

What Phaze carries forward Phaze’s compiler (@madenowhere/phaze-compile) does some of what Svelte’s compiler does — auto-wrapping ternaries and JSX expressions for reactivity. Phaze doesn’t go as far (no $state syntactic primitive); the user still calls signal() explicitly. Phaze splits the difference: enough compiler to remove user-visible warts, not so much that the runtime is opaque.


Standardization is in motion. Stage 1 at the time of writing.

What it’s bringing

  • A standard Signal.State and Signal.Computed shape every framework can target.
  • A common subscription protocol so multiple signal libraries can interop in the same app.

What’s still TBD

  • The exact API shape (function-call vs .value vs property-access).
  • How “effects” / autorun integrate with the host renderer.
  • Async / resource semantics.

What Phaze will likely carry forward Phaze’s primitives are intentionally close to the TC39 shape. If/when the proposal lands, Phaze aims to be a compliant implementation rather than a competing one.


Phaze isn’t claiming originality on signals. The pattern is a 15-year-old algorithm; using it in 2026 is the obvious choice for a UI runtime. What Phaze contributes is a particular synthesis of the ideas above plus a few new ones:

Carried forward fromWhat
Knockout (2010)Auto-tracking algorithm
S.js (2014)Function-call read API
MobX (2015)The signal / computed / effect triad shape; deep-proxy stores (as phaze/store)
Vue 3 (2020)Reactive object proxies, standalone primitives outside components
Solid (2018)Compiler emit, fine-grained DOM bindings, <For> keyed reconciliation, error-boundary-as-hook
Svelte 5 (2024)Compiler that auto-wraps reactive expressions in JSX

What’s distinctly Phaze:

  1. Reactive signals integrated with modern browser primitives that none of the predecessors marry:

    • Element.moveBefore() for state-preserving keyed reorders.
    • AbortSignal for auto-cleanup of fetches and listeners.
    • scheduler.postTask for priority-aware effects.
  2. Aggressive minimalism on the JSX surface. Phaze ships three flow components — <For>, <Catch>, <Portal> — and actively deletes the rest. <Show>, <Switch>, <Match>, <Dynamic> are all replaced by computed() + native control flow. None of the predecessors did this trim. Solid still ships all of them.

  3. The “truly reactive” framing as the value prop. Naming the architectural difference with React directly: React re-renders, Phaze doesn’t. Most predecessors stayed quieter on the comparison.

  4. No createContext. Module-level signals replace it entirely. No predecessor took this stand.

  5. Race-safe signal.async() baked in. Solid’s createResource is similar; Phaze’s loader gets the abort-on-rerun semantic via the runtime’s abortSignal() change.

  6. Smaller bundle than the predecessors we can measure:

    • Preact + @preact/signals: 5.31 KB brotli (measured, bench/benchmark/)
    • React + react-dom: 50.49 KB brotli (same bench)
    • Solid 1.x: ~6 KB brotli (unmeasured — no bench frame yet)
    • Phaze: sub-3 KB brotli (runtime alone via pnpm size)
  7. SSR + hydration inside the runtime, no separate package. Phaze ships server-side rendering and hydration as part of the sub-3 KB brotli runtime — no phaze-ssr package to install, no bundle delta to flip an island from client:only to client:load. React requires react-dom/server; Preact requires preact-render-to-string; Solid has a separate solid-js/web SSR path. Phaze’s hydration is a ~200-line cursor-stack walker that shares the JSX runtime with render() — same code, different mode. See SSR & hydration.

  8. Compiler that removes user-visible reactive boilerplate. {cond ? <A/> : <B/>} works without manual thunk wrapping. Solid’s compiler does similar work; Phaze’s is smaller-scoped (no full DOM-emit; uses the runtime).


What didn’t survive (across the whole lineage)

Section titled “What didn’t survive (across the whole lineage)”

A list of patterns that were tried and shed:

  • Decorators for reactivity (MobX) — JS decorators stalled.
  • Component-instance-tied reactivity (Vue 2) — broken by Vue 3’s Composition API.
  • HTML-attribute data binding (Knockout) — JSX won.
  • Class-based reactive state (MobX, Vue 2) — functional / closure approaches won.
  • Strict synchronous propagation (S.js) — too hard to tame; everyone batches now.
  • [get, set] tuple signals (Solid) — most newer libs use a single value with methods.
  • .value accessor for reads (Preact, Vue) — function-call shape (count()) won the wider race in Solid, Angular, TC39.
  • Many named flow components (Solid: <Show>, <Switch>, <Match>, <Dynamic>) — Phaze deletes most as redundant.
  • createContext for shared state (React, Preact, Solid, Angular all have it) — Phaze deletes; module signals replace it.

This isn’t framework triumphalism — every one of those patterns made sense in its time. They just turned out to be replaceable as the broader pattern matured.

The lineage is not done. Things that nobody — including Phaze — has fully solved:

  • Cross-framework signal interop. TC39 is trying. Today, signals from @preact/signals and Solid don’t share a subscription protocol.
  • Async + Suspense semantics. React’s <Suspense> is incomplete; Solid’s createResource works but isn’t streaming-friendly. Phaze’s signal.async is race-safe via abortSignal() and reactively tracks dependencies; coordinating with a fallback-boundary model at the framework runtime layer is a future direction.
  • Standardization — how does it land if/when TC39 adopts.

Phaze will move on these in subsequent milestones.