Synchronous, fine-grained reactivity for JavaScript and TypeScript. Framework-agnostic. ESM-only. No magic, no scheduler, no virtual graph.
import {createSignal, createEffect} from '@spearwolf/signalize';
const count = createSignal(0);
createEffect(() => console.log('count =', count.get()));
// => "count = 0"
count.set(5);
// => "count = 5"All modern signal systems give you synchronously consistent reads: call
derived.get() after a set() and you get the recomputed value, glitch-free.
That part is table stakes — Solid, Vue, Angular, Svelte 5, Preact Signals,
MobX, and signalize all guarantee it.
What differs is effects — the observer callbacks you register to react
to a change (write to the DOM, push to a socket, mutate an external system).
Vue's watch/watchEffect, Angular's effect(), and Svelte 5's $effect
defer these to a microtask or the next change-detection tick; MobX runs
reactions at the end of the enclosing action. So while the value you'd
read is already correct, the side effects that depend on it haven't fired
yet — and you have no in-band hook to wait for them without yielding the
call stack.
signalize runs every dependent effect inline, in deterministic priority
order, on the same call stack as the write — no scheduler, no microtask, no
batch boundary. After set() returns, every observer has already executed.
This is the central design trade-off:
const total = createSignal(0);
createEffect(() => console.log('total =', total.get()));
// => "total = 0"
total.set(5);
// => "total = 5" ← already logged before this line finishedThat property is why this library exists. It makes reactive logic safe to
embed inside a requestAnimationFrame callback, a physics tick, a worker
message handler, or any other place where "settle before I move on" matters
more than "batch for free". When you do want batching, you ask for it
explicitly with batch().
In complex interactive front-ends—such as 3D configurators, real-time dashboards, or gamified PWAs—managing state with traditional tools often leads to "render hell" or unpredictable side effects.
signalize was architected to solve these specific challenges by providing a precise and decoupled reactive core that works independently of any UI framework. It is designed for developers who need full control over when and how state changes propagate through their systems.
- 🔌 Zero Framework Lock-in: Use it with React, Vue, Web Components, or Vanilla JS. It’s the "source of truth" that stays stable even if you migrate your UI layer.
- 🎯 Precise Reactivity: No global re-renders. Only the specific observers (effects) that depend on a changed signal are executed.
- 🛠 Production-Ready: Developed by a Software Artisan with 20+ years of experience to power mission-critical industrial applications.
- Four primitives —
signal(state),effect(observer),memo(cached derive),link(signal-to-signal binding). Small surface, no DSL. - Inline propagation —
set()runs every dependent effect before it returns. No scheduler, no microtask, no virtual graph. - Priority-ordered effects — numeric priority (memos at
1000, regular effects at0); runaway loops fail loud atmaxDepth = 256. - Auto-tracked dependencies — subscribe on read, unsubscribe when no longer read; nested effects tear down before their parent re-runs.
- Lifecycle bundles —
SignalGroupties signals, effects, and links to a host object and disposes them in one call; counters likegetSignalsCount()make leaks assertable in tests. - Context modes —
batch()to coalesce writes,beQuiet()for silent mutation,hibernate()to pause reactivity,value()/.valuefor untracked reads. - Optional class API — TC39 standard
@signal/@memodecorators on a separate subpath; the core has no class dependency. - TypeScript-first — every primitive, option, and decorator is fully typed.
Runs anywhere modern JavaScript runs. Targets ES2023, requires Node >=24.13.
npm install @spearwolf/signalize@spearwolf/eventize 🏹 is a peer dependency.
// signals
createSignal, destroySignal, isSignal, muteSignal, unmuteSignal,
getSignalsCount, touch, value
// effects
createEffect, getEffectsCount, onCreateEffect, onDestroyEffect
// memos
createMemo
// links
link, unlink, getLinksCount
// context modes
batch, beQuiet, isQuiet, hibernate
// lifecycle / collections
SignalGroup, getSignalGroupsCount, SignalAutoMap
// host-object signals
findObjectSignalByName, findObjectSignals, findObjectSignalNames,
destroyObjectSignals
// decorators (subpath: '@spearwolf/signalize/decorators')
signal, memoInline propagation means the HUD update runs inside the frame that produced the new value — no risk of seeing a stale FPS counter one frame later.
import {createSignal, createMemo, createEffect} from '@spearwolf/signalize';
const fps = createSignal(60);
const label = createMemo(() => `FPS: ${fps.get().toFixed(0)}`);
createEffect(() => hud.textContent = label());
function frame(now: number) {
fps.set(measureFps(now)); // HUD updates synchronously, before next line
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);createMemo caches a derived value and runs at high priority — every effect
that reads the memo will see a fully-recomputed result. batch() makes a
group of writes look like a single transaction to downstream effects.
import {createSignal, createMemo, createEffect, batch} from '@spearwolf/signalize';
const price = createSignal(100);
const qty = createSignal(1);
const total = createMemo(() => price.get() * qty.get());
createEffect(() => console.log('total =', total.get()));
// => "total = 100"
batch(() => {
price.set(120);
qty.set(3);
});
// => "total = 360" (one log, not two)Attach signals and effects to a host object; tear them down with a single call. Useful for Web Components, classes, or any object with a clear dispose point.
import {createSignal, createEffect, SignalGroup} from '@spearwolf/signalize';
class CartWidget extends HTMLElement {
count = createSignal(0, {attach: this});
connectedCallback() {
createEffect(() => this.render(this.count.get()), {attach: this});
}
disconnectedCallback() {
SignalGroup.delete(this); // destroys count + the effect + their subscriptions
}
render(n: number) { this.textContent = `${n} items`; }
}Reactivity lives in the model, not the view. The same store can drive a React adapter, a Vue component, a Web Component, or a plain DOM render — none of them have to know about each other.
import {createSignal, createEffect} from '@spearwolf/signalize';
export function createCart() {
const items = createSignal<Item[]>([]);
const subtotal = createMemo(() =>
items.get().reduce((sum, i) => sum + i.price * i.qty, 0),
);
return {
add: (i: Item) => items.set([...items.get(), i]),
items, subtotal,
};
}
// Anywhere — React hook, Vue ref bridge, vanilla:
const cart = createCart();
createEffect(() => console.log('subtotal =', cart.subtotal()));signalize was built for — and is in production use across —
3D configurators, real-time dashboards, gamified PWAs, and game/render
loops. More broadly, it fits any scenario where you want:
- a stable reactive core that survives swapping out the UI layer,
- predictable propagation that you can reason about line by line, or
- lifecycle-aware bundles of reactive state attached to long-lived host objects (entities, components, panels, widgets).
It is not aimed at "magic full-stack reactivity" or implicit re-render trees. If your mental model is "I want my UI to re-render automatically and I'll never think about subscriptions" — a framework-bound solution will be less effort.
import {signal, memo} from '@spearwolf/signalize/decorators';
class Counter {
@signal() accessor value = 0;
@memo() doubled() { return this.value * 2; }
inc() { this.value++; }
}The decorator API uses TC39 standard decorators (no
experimentalDecorators). Memos created via@memoare always lazy.
| Document | Purpose |
|---|---|
| Quickstart | Install + 5-minute tour. |
| Architecture | Concepts, internals, source map. |
| API reference | Every export, every option. |
| Recipes & quirks | Patterns, gotchas, lifecycle. |
| Cheat sheet | One-page lookup. |
For changes between releases, see CHANGELOG.md.
Issues and pull requests are welcome. See CONTRIBUTING.md and CODE_OF_CONDUCT.md.
Apache-2.0. See LICENSE.
