Svelte Reactive Statements: Stop Manually Syncing Derived Values
Svelte's $: reactive statements turn derived state from an update-everywhere liability into a declare-once guarantee. Here's how they work, a working counter example, and the performance trade-off to watch.
28 Aug 2025, 11:29 UTC

If you've ever written a component where one value depends on another — a doubled count, a formatted price, a validation message — you've felt the temptation to update it imperatively. Click handler increments count, then you remember to also recompute double, then you add a third derived value and forget to update it in one of the four places count changes. The UI drifts out of sync, and the bug only shows up in the path you didn't test.
Svelte's reactive statement syntax, $:, exists to delete that entire class of bug. You declare how a value is derived once, and Svelte re-runs that declaration whenever any value it references changes. This post walks through how it works, a concrete example, and the performance trade-off you should know about before sprinkling $: everywhere.
What $: actually does
In Svelte 3 and 4, any top-level statement in a component's <script> prefixed with $: is treated as reactive. During compilation, Svelte analyzes which variables the statement reads, and schedules the statement to re-run after any of those variables are reassigned. You don't list dependencies manually the way you would with a React useEffect dependency array — the compiler figures it out, which means it can't go stale when you refactor.
The mental model is a spreadsheet cell: double is a formula over count, not a snapshot. Change count anywhere — a button handler, an interval, a prop update from a parent — and double recalculates before the next render.
A worked example: counter with derived state
Here's a small component that shows the count, its double, and a warning when the count gets too high. Without $:, you'd be recomputing both derived values inside every mutation site. With it, the derivation lives in exactly one place:
<script>
let count = 0;
$: double = count * 2;
$: warning = count > 10 ? 'Too high — consider resetting' : '';
function increment() {
count += 1;
}
</script>
<button on:click={increment}>Increment</button>
<p>Count: {count}, double: {double}</p>
{#if warning}
<p class="warning">{warning}</p>
{/if}Click the button and both double and the warning stay in sync automatically. Note the second statement: reactive statements aren't limited to simple assignments. Any statement works — if blocks, loops, function calls — as long as it's a single statement at the top level of the script.
To try this yourself: scaffold a project with npm create svelte@latest my-app (requires Node.js and npm), replace the contents of the main page or App.svelte with the code above, then run npm run dev and open the printed local URL. Clicking Increment should update the doubled value and reveal the warning past ten. Check your Svelte version with npm list svelte — the $: syntax described here applies to Svelte 3 and 4. (Svelte 5 introduces runes such as $derived as the successor to this pattern; if you're on 5, look there instead.)
The trade-off: it runs more often than you think
Reactive statements re-run after every state change that touches their dependencies, on each microtask flush. Two consequences follow.
First, expensive derivations get expensive fast. A $: statement that filters a 50,000-row array will re-filter on every keystroke if its dependency is a search box. For heavy work, memoize, debounce the input, or push the computation into a derived store shared at the module level.
Second, ordering and batching can surprise you. If a function reassigns several variables in sequence, dependent reactive statements may run multiple times within that flush. Svelte orders statements by dependency (topologically), but if you mutate state in a tight loop, you'll trigger repeated work. Batching mutations or awaiting tick() where appropriate keeps this predictable.
There's also a scope limit: $: lives inside one component. If the same derivation is needed in three components, duplicating the reactive statement duplicates the work and the maintenance. That's the signal to extract a derived store instead.
How to check it's behaving
Verification is cheap. Open browser devtools and confirm there are no compile warnings about the statement. Interact with the component and watch the DOM update without any manual sync code. If you're on SvelteKit, view the server-rendered page source: reactive statements run during SSR too, so the initial HTML should already contain the correct derived values. For performance concerns, record a trace in the Chrome Performance tab while interacting; a simple derivation like the example above should not produce long frames. If it does, the computation — not the reactivity — is your problem.
The takeaway
Reach for $: whenever a value is a pure function of other component state. It converts an update-everywhere liability into a declare-once guarantee. Just keep the derivations cheap, extract shared logic into stores, and remember that Svelte 5's runes change the spelling of this pattern even though the idea — derived state should be declared, not maintained — stays the same.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.