Svelte `$:` reactive statements: what the compiler actually tracks
Svelte `$:` statements re-run when the variables they read are reassigned. Here is the mechanism, a worked example, and the mistakes that break derived values.
14 Sept 2025, 00:17 UTC

If a derived value in Svelte stops updating, the cause is usually not the $: syntax itself. It is that Svelte reactivity is triggered by assignment, not by mutation. A reactive statement re-runs when the variables it reads are reassigned; changing a property inside an object or array does nothing until the container variable is reassigned or another dependency changes.
Useful takeaway: treat $: as a compile-time dependency declaration for values derived from component state. Use it for calculations from variables you reassign. Do not use it as a deep watcher, a global event bus, or a place for expensive side effects.
What $: compiles to
In Svelte 4, a statement such as $: doubled = count * 2 is not a runtime observer. The compiler scans the statement, records the component variables it reads, and emits an update function. When count is assigned, the component marks itself dirty and runs that update function before the DOM is patched.
That has two consequences. First, the dependency list is static: it contains variables, not property paths. Second, the statement runs whenever any listed variable is invalidated, even if the new value would produce the same result.
Worked example: a derived total that goes stale
This component uses Svelte 4 syntax. It shows the normal case and the common failure case.
<script>
let count = 0;
let items = [{ id: 'a', qty: 1 }];
// Re-runs when `count` is reassigned.
$: doubled = count * 2;
$: status = count > 10 ? 'high' : 'normal';
// Depends on the `items` variable, not on `items[0].qty`.
$: total = items.reduce((sum, item) => sum + item.qty, 0);
function increment() {
count += 1;
}
function addItem() {
items = [...items, { id: String(items.length + 1), qty: 1 }];
}
function bumpFirst() {
items[0].qty += 1;
}
</script>
<button on:click={increment}>count: {count}</button>
<p>doubled={doubled} status={status}</p>
<button on:click={addItem}>add item</button>
<button on:click={bumpFirst}>bump first qty</button>
<p>total={total}</p>
</code></pre>
In Svelte 4, clicking add item reassigns items, so total re-runs. Clicking bump first qty mutates items[0].qty without reassigning items, so total keeps its previous value until some other assignment invalidates items. The fix is to reassign the container, for example items = items or items = [...items], or to move the mutable data into a store with an explicit update method.
Where $: stops working
Approach Use it when Main limitation
$: value = ...You need a value derived from component variables that are reassigned. Dependencies are static; nested mutations are invisible.
Plain function call in markup The calculation is cheap and you do not need to reuse it elsewhere. It runs during render, so it is not a cached reactive value.
Writable/derived store State must cross component boundaries or be updated from many places. You must manage subscriptions and update contracts.
onMount / afterUpdateYou need DOM measurements, subscriptions, or imperative side effects. Not for pure derived values; ordering and cleanup matter.
Static dependencies are not deep dependencies
A statement like $: label = items[0].name depends on items, not on items[0].name. Reassigning items re-runs the statement; renaming the first item in place does not. The same applies to $: total = items.reduce(...): the dependency is the array binding.
Adding a dependency means changing the statement
If a reactive statement should also re-run when filter changes, the statement must read filter. Adding a defined variable is legal and changes the invalidation set. Referencing a name that is not declared in the component is a compile problem, not a silent global subscription. Treat the exact diagnostic as version-specific and verify with your installed compiler.
Heavy expressions run on every invalidation
Because the statement re-runs whenever any dependency is assigned, an expensive calculation can run more often than expected. Move heavy work into a function that is called only when needed, debounce the input, or use a derived store if the value is shared. In Svelte 4, calling a function inside $: does not memoize the function result by itself.
Ordering with bind:this and lifecycle hooks
Reactive statements run as part of the update cycle, before the DOM patch is finished. Do not rely on bind:this element dimensions inside $: during the first run. Measure in afterUpdate, or use await tick() when you need the DOM to settle.
Scope and SSR
$: is component-local. It cannot directly update a parent component or a global store. For parent/child data flow, derive from props; for shared state, use stores. During server-side rendering, pure derived assignments still run because the component instance is created to produce HTML. Side effects that touch window, document, or timers should be guarded or moved to onMount.
Practical checks
Run these in a terminal in the project root. Normal user permissions are enough; you do not need sudo. If your Svelte project is in <project-dir>, run them there.
# Confirm the installed Svelte version before relying on syntax behavior
npm ls svelte
# Start the dev server; stop it with Ctrl+C
npm run dev
# If the default port is busy, choose another port
npm run dev -- --port 5174
# Compile the component; this is the check for invalid dependencies
npm run build
Expected checks: npm ls svelte prints the resolved Svelte version; npm run dev starts a local server; clicking count updates doubled and status; clicking add item updates total; clicking bump first qty leaves total unchanged until items is reassigned. A temporary $: console.log(count) should log once per relevant invalidation; remove it before committing.
Risk note: npm run build writes build output and may fail if the project mixes Svelte versions. If npm ls svelte lists more than one version, resolve that before interpreting compiler behavior.
Version note
These examples assume Svelte 4.x. Svelte 5 changes the recommended reactivity model with runes such as $state and $derived. If package.json shows Svelte 5, treat $: as legacy syntax and confirm the migration status of your codebase before adding new reactive statements.