Reactivity
Tanni's reactivity is built on signals. A signal holds a value and tracks which
computations read it; when the value changes, only those computations re-run. All of
the primitives below are imported from tannijs.
import { createSignal, createEffect, createMemo, batch, untrack } from 'tannijs';
createSignal
function createSignal<T>(initialValue: T): [Accessor<T>, Setter<T>];
Creates a reactive value. Returns a [getter, setter] tuple:
const [count, setCount] = createSignal(0);
count(); // read → 0
setCount(5); // write → 5
setCount((c) => c + 1); // functional update → 6
- Read by calling the getter:
count(). Reading inside an effect or memo registers a dependency. - Write by calling the setter with a new value or an updater function.
- Updates are skipped when the new value is identical to the current one
(compared with
Object.is), so setting a signal to its existing value won't trigger re-runs.
createEffect
function createEffect(fn: () => void): void;
Runs fn immediately and re-runs it whenever any signal it read changes:
const [name, setName] = createSignal('Tanni');
createEffect(() => {
console.log('Name is', name());
});
setName('World'); // logs: "Name is World"
Dependencies are tracked automatically on each run, so effects always reflect the
signals they currently read. effect is exported as an alias for createEffect.
createMemo
function createMemo<T>(fn: () => T): Accessor<T>;
Creates a cached derived value. The memo recomputes only when one of its dependencies changes, and notifies its own subscribers only when the result actually changes:
const [count, setCount] = createSignal(2);
const doubled = createMemo(() => count() * 2);
doubled(); // 4
setCount(5);
doubled(); // 10
Use a memo (rather than calling a function inline) when a derived value is expensive or read in several places.
batch
function batch<T>(fn: () => T): T;
Groups multiple signal writes so dependent computations run once after the batch completes, instead of after each individual write:
const [first, setFirst] = createSignal('Ada');
const [last, setLast] = createSignal('Lovelace');
createEffect(() => console.log(first(), last()));
batch(() => {
setFirst('Grace');
setLast('Hopper');
}); // effect runs a single time after both updates
untrack
function untrack<T>(fn: () => T): T;
Reads signals without creating a dependency. Useful inside an effect when you want the current value of a signal but don't want changes to it to re-trigger the effect:
createEffect(() => {
// re-runs when `a` changes, but NOT when `b` changes
console.log(a(), untrack(() => b()));
});
Lifecycle helpers
onMount and onCleanup also live in the reactivity module. They're covered in
detail on the Lifecycle page.