Reference: Reactivity macros

Reference

Reactivity macros

Macros look like function calls, but the compiler replaces them: none of them exists at run time. They work in .tsx, .jsx, .ts and .js files. Types for them come from @sigil-dev/types, which a new project already lists in tsconfig.json.

A macro must initialise a variable declaration (let x = $state(0)), except $effect and $inspect, which are statements.

$state

let count = $state(0);
let user = $state({ name: "Ada", tags: ["admin"] });

Declares reactive state. Reading it inside JSX, $derived or $effect subscribes to it; assigning it notifies subscribers.

  • Declare it with let: assignment is how you change a primitive (count++, count = 5).
  • Objects and arrays become deeply reactive proxies. Changing any property at any depth (user.name = "Grace", user.tags.push("x")) updates what read it. Array methods that mutate (push, splice, sort…) work.
  • Replacing the whole value (user = { … }) works too.

Inside a component, each instance gets its own state. At the top level of a module, all its readers share one value; on the server that means every request shares it (Sharing state).

$state.raw

let rows = $state.raw<Row[]>([]);

State without the deep proxy: only assigning the variable notifies (rows = [...rows, row]); mutating what's inside (rows.push(row)) does not. Use it for large data you replace wholesale, where proxying every element would be wasted work.

$state.snapshot

const plain = $state.snapshot(user);

A deep copy of a state value as plain objects and arrays, without proxies. Use it to hand state to code that shouldn't see proxies: structuredClone, postMessage, a library that compares identities, or console.log.

$store

export let cart = $store({ items: [] as string[] });

The same as $state, for module-level state that other modules import. The only difference is that the compiler doesn't warn when the declaring file never reads it. Importers can read it and mutate its contents; assigning the binding itself only works in the declaring module (Sharing state).

$derived

let count = $state(2);
let doubled = $derived(count * 2);
let label = $derived(doubled === 1 ? "1 item" : `${doubled} items`);

A value computed from other reactive values, recomputed when they change. It's read-only: assign the state it derives from instead. The expression should be free of side effects; use $effect for those.

$effect

let query = $state("");

$effect(() => {
	document.title = query ? `Search: ${query}` : "Search";
});

Runs a function in the browser, and again whenever state it read changes. It never runs on the server.

Return a function to clean up. It runs before the effect re-runs and when the component is torn down (for example, when the router navigates away):

let delay = $state(1000);
let ticks = $state(0);

$effect(() => {
	const id = setInterval(() => ticks++, delay);
	return () => clearInterval(id);
});

Only what the function reads while it runs is tracked. A read inside a callback it sets up (setInterval's callback above) isn't; that's why changing ticks doesn't restart the timer.

$effect.pre

Like $effect, but runs before regular effects on each update, for work that has to happen before the DOM is touched, such as measuring scroll position.

$effect.tracking

$effect.tracking() returns true when called while an effect or derived value is running, false otherwise.

$inspect

let count = $state(0);
$inspect(count);
$inspect(count).with((values) => console.trace(values));

Logs its arguments whenever they change, or passes them to the .with callback. It's a debugging aid: it's dropped from server builds, but not from browser bundles, so remove it before shipping.

Untracked reads

To read state inside an effect without subscribing to it, wrap the read in untrack from @sigil-dev/runtime (Runtime).

Edit this page