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.
Timeline at a glance
Section titled “Timeline at a glance”| year | library | shipped |
|---|---|---|
| 2010 | Knockout.js | ko.observable() + ko.computed() |
| 2014 | S.js | Pure synchronous signal primitive |
| 2015 | MobX | observable, computed, autorun |
| 2016 | Vue 2 | Reactive data() properties |
| 2018 | SolidJS | Modern “signal” terminology + JSX integration |
| 2020 | Vue 3 | ref(), computed(), reactive() (Composition API) |
| Sep 2022 | @preact/signals | signal() with .value accessor |
| May 2023 | Angular Signals | signal(), computed(), effect() (Angular 16) |
| 2024 | Svelte 5 (runes) | $state, $derived, $effect (compiler-emitted) |
| 2024+ | TC39 Signals proposal | Cross-framework standardization (Stage 1) |
Knockout.js (2010)
Section titled “Knockout.js (2010)”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.
S.js (2014)
Section titled “S.js (2014)”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()notcount.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.
MobX (2015)
Section titled “MobX (2015)”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 shapeeffect()later re-took.)- Action-based mutations — wraps writes in
runInActionto batch. - The
observable/computed/autoruntriad — 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’sphaze/store).
What Phaze carries forward
The triad shape: signal ↔ observable, computed ↔ computed, effect ↔ autorun. Same primitives, smaller surface. Plus Phaze’s optional phaze/store resurrects the deep-proxy idea as an opt-in.
Vue 2 → Vue 3 (2016 → 2020)
Section titled “Vue 2 → Vue 3 (2016 → 2020)”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 viaObject.defineProperty. - The
computed:block in components.
What Vue 3 brought
ref(value)— standalone signal-shaped reactive ref. (.valueaccessor — 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/computedstandalone. - 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.
SolidJS (2018)
Section titled “SolidJS (2018)”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.createResourcefor 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.
@preact/signals (Sep 2022)
Section titled “@preact/signals (Sep 2022)”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
.valueaccessor (read and write viacount.value). A divergence from Solid’scount()and Angular’s latercount(). - 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
.valueaccessor — not adopted by Solid, Angular, or TC39. The function-call shape won. - Shipping signals as a separate package (
@preact/signalsnotpreact) — 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.
Angular Signals (May 2023)
Section titled “Angular Signals (May 2023)”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.
Svelte 5 runes (2024)
Section titled “Svelte 5 runes (2024)”Compiler-only signals. The same pattern, but the compiler emits all the wiring; users see
$statelike 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
$runesyntactic magic catches on outside Svelte. Other frameworks haven’t picked up$stateas 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.
TC39 Signals proposal (2024+)
Section titled “TC39 Signals proposal (2024+)”Standardization is in motion. Stage 1 at the time of writing.
What it’s bringing
- A standard
Signal.StateandSignal.Computedshape 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
.valuevs property-access). - How “effects” /
autorunintegrate 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.
Where Phaze fits
Section titled “Where Phaze fits”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 from | What |
|---|---|
| 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:
-
Reactive signals integrated with modern browser primitives that none of the predecessors marry:
Element.moveBefore()for state-preserving keyed reorders.AbortSignalfor auto-cleanup of fetches and listeners.scheduler.postTaskfor priority-aware effects.
-
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 bycomputed()+ native control flow. None of the predecessors did this trim. Solid still ships all of them. -
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.
-
No
createContext. Module-level signals replace it entirely. No predecessor took this stand. -
Race-safe
signal.async()baked in. Solid’screateResourceis similar; Phaze’s loader gets the abort-on-rerun semantic via the runtime’sabortSignal()change. -
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)
- Preact +
-
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-ssrpackage to install, no bundle delta to flip an island fromclient:onlytoclient:load. React requiresreact-dom/server; Preact requirespreact-render-to-string; Solid has a separatesolid-js/webSSR path. Phaze’s hydration is a ~200-line cursor-stack walker that shares the JSX runtime withrender()— same code, different mode. See SSR & hydration. -
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..valueaccessor 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. createContextfor 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.
What’s still open
Section titled “What’s still open”The lineage is not done. Things that nobody — including Phaze — has fully solved:
- Cross-framework signal interop. TC39 is trying. Today, signals from
@preact/signalsand Solid don’t share a subscription protocol. - Async + Suspense semantics. React’s
<Suspense>is incomplete; Solid’screateResourceworks but isn’t streaming-friendly. Phaze’ssignal.asyncis race-safe viaabortSignal()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.
References
Section titled “References”- Knockout.js documentation — https://knockoutjs.com/documentation/observables.html
- “SolidJS Creator on Fine-Grained Reactivity” — https://thenewstack.io/solidjs-creator-on-fine-grained-reactivity-as-next-frontier/
- @preact/signals announcement — https://preactjs.com/blog/introducing-signals/
- Angular Signals documentation — https://angular.dev/guide/signals
- TC39 Signals proposal — https://github.com/tc39/proposal-signals
- Vue Composition API reference — https://vuejs.org/api/reactivity-core.html
- Svelte 5 runes — https://svelte.dev/docs/svelte/what-are-runes