Signals
Signals are Whisq’s reactive primitives. They hold values that automatically track dependencies and trigger updates when changed.
signal() — Reactive State
Section titled “signal() — Reactive State”import { signal } from "@whisq/core";
const count = signal(0);
count.value; // 0 — read (triggers dependency tracking)count.value = 5; // write (triggers updates)count.update(n => n + 1); // 6 — update via functioncount.peek(); // 6 — read WITHOUT trackingcount.set(10); // 10 — alias for direct assignmentWhen to use peek()
Section titled “When to use peek()”Use peek() inside effects when you need to read a value without creating a dependency:
effect(() => { // Re-runs when `trigger` changes, but NOT when `config` changes console.log(trigger.value, config.peek());});computed() — Derived Values
Section titled “computed() — Derived Values”computed() creates a read-only signal that auto-updates when its dependencies change:
import { signal, computed } from "@whisq/core";
const firstName = signal("Ada");const lastName = signal("Lovelace");const fullName = computed(() => `${firstName.value} ${lastName.value}`);
fullName.value; // "Ada Lovelace"
firstName.value = "Grace";fullName.value; // "Grace Lovelace" — auto-updatedComputed values are lazy — they don’t recompute until read. They also cache — reading twice without dependency changes returns the cached value.
effect() — Side Effects
Section titled “effect() — Side Effects”effect() runs a function immediately and re-runs it whenever its dependencies change:
import { signal, effect } from "@whisq/core";
const count = signal(0);
const dispose = effect(() => { console.log(`Count is: ${count.value}`);});// Logs: "Count is: 0"
count.value = 1;// Logs: "Count is: 1"
dispose(); // Stop watchingcount.value = 2; // No log — effect is disposedCleanup Functions
Section titled “Cleanup Functions”Return a function from an effect to run cleanup before each re-execution:
effect(() => { const timer = setInterval(() => tick(), 1000); return () => clearInterval(timer); // cleanup});Conditional Dependencies
Section titled “Conditional Dependencies”Effects only track signals read in the current execution:
const flag = signal(true);const a = signal("A");const b = signal("B");
effect(() => { // When flag is true, tracks `a` only // When flag is false, tracks `b` only console.log(flag.value ? a.value : b.value);});batch() — Grouped Updates
Section titled “batch() — Grouped Updates”batch() defers effect re-runs until all updates complete:
import { signal, effect, batch } from "@whisq/core";
const x = signal(0);const y = signal(0);
effect(() => { console.log(`${x.value}, ${y.value}`);});// Logs: "0, 0"
batch(() => { x.value = 1; y.value = 2;});// Logs: "1, 2" — only ONCE, not twiceWithout batch(), the effect would run once for x and again for y.
subscribe() — Manual Subscriptions
Section titled “subscribe() — Manual Subscriptions”For integrating with external systems:
const count = signal(0);
const unsub = count.subscribe(value => { // Called immediately with current value, then on every change externalSystem.update(value);});
unsub(); // Stop subscribingGetters and accessors
Section titled “Getters and accessors”Two () => T shapes come up constantly in Whisq prose and code. They look structurally identical but fill different roles — which matters because it controls whether you need to write the function or whether the framework hands it to you:
-
Getter — a
() => valueclosure you write around a read to make it reactive. Whisq evaluates it lazily whenever the surrounding position needs to re-render. You’ll see getters in reactive element children, reactive props, computed bodies, effect bodies,rcx()args, and anywhere the docs say “pass a getter.”span(() => count.value) // getter childdiv({ class: () => active.value ? "on" : "off" }) // getter proprcx("btn", () => loading.value && "btn-loading") // getter arg -
Accessor — a
() => Tfunction the framework hands to you. You call it (with parentheses) to read the current value. The framework owns a backing signal; when it updates, anything that reads the accessor inside a getter or reactive position sees the fresh value. You’ll see accessors on keyedeach()callbacks (todo(),index()) and onresource()fields (users.data(),users.loading(),users.error()).each(() => todos.value, (todo) => li(todo().text), { key: (t) => t.id })// ↑ `todo` is an accessor — call it to read the current entry// `key` receives plain T, not an accessorconst users = resource(() => fetch("/api/users").then(r => r.json()));users.data() // accessor — returns current data or undefinedusers.loading() // accessor — true while fetching
Rule of thumb: “who wrote the function?” User-written → getter. Framework-supplied → accessor. When you hear “wrap it in a getter” it means you’re writing a closure; when you hear “call the accessor” it means the framework gave you the function and you call it.
Edge case — rcx(). rcx() is framework-supplied and returns a () => string that lives in a reactive class position. Its return value is passed to a getter-shaped slot, so the docs describe it as a “getter function” at the call site even though the framework produced it. The distinction only matters when the framework gives you a function and tells you to call it to read a value — that’s the accessor pattern. If the framework gives you a function that the framework itself invokes at a reactive position, it behaves as a getter.
Signals (.value) and plain values are the other two shapes that show up in reactive positions. See the Reactive shapes cheat sheet for all four.
Next Steps
Section titled “Next Steps”- How Reactivity Works — The diagram + five questions for the signal → subscriber → DOM update flow.
- Elements — Use signals in reactive UI elements
- Components — Encapsulate signals in components
Docs current to v0.1.0-alpha.9 . All releases →