Svelte 5 Runes Explained: $state, $derived, $effect (2026)
June 19, 2026
Runes are Svelte 5's explicit reactivity primitives, written with a $ prefix. Use $state to declare reactive values, $derived to compute values from other state, and $effect only for side effects like network calls or DOM work. Reach for $derived before $effect.
TL;DR
Svelte 5 (stable since October 19, 2024; 5.56.x as of June 2026) replaced the compiler's implicit reactivity with runes — function-like symbols such as $state, $derived, and $effect.12 The mental shift is simple: $state holds values, $derived computes values, and $effect runs side effects. A common mistake is using $effect to sync one value to another — the docs explicitly advise against it; use $derived instead.3 Svelte 4 stores still work, so migration can be gradual.4
What you'll learn
- What runes are and why Svelte 5 introduced them
- How
$stateworks, including deep reactivity,$state.raw, and$state.snapshot - How
$derivedand$derived.byreplace$:computed values - What
$effectis for — and the cases where you should not use it - A decision rule for
$derivedvs$effect - How
$propsand$bindablereplaceexport let - Whether Svelte stores still work, and how to migrate Svelte 4 reactivity to runes
What are runes in Svelte 5?
Runes are special symbols, prefixed with $, that tell the Svelte compiler how reactivity works. They look like function calls but are compiler keywords — you don't import them, and they only have meaning inside .svelte, .svelte.js, and .svelte.ts files.4 Svelte 5 ships seven runes: $state, $derived, $effect, $props, $bindable, $inspect, and $host.5
Before runes, Svelte 4 made every top-level let in a component reactive and used the $: label for computed values. That was concise but magical: reactivity depended on where code lived. Runes make reactivity explicit and portable — the same $state works inside a component or inside a plain .svelte.js module — which is why you can now keep shared reactive logic outside components entirely.4
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>
<button onclick={() => count++}>
clicked {count} times — doubled is {doubled}
</button>
How does $state work in Svelte 5?
$state declares a reactive value: when it changes, the UI that reads it updates. It is the direct replacement for a plain reactive let from Svelte 4.5
<script>
let count = $state(0);
let user = $state({ name: 'Ada', tags: ['admin'] });
</script>
<button onclick={() => count++}>{count}</button>
<button onclick={() => user.tags.push('editor')}>add tag</button>
The important detail: objects and arrays passed to $state become deeply reactive proxies. Mutating a nested property — user.tags.push(...) above — triggers updates, with no reassignment needed.6 That is a real change from Svelte 4, where you had to reassign (user = user) to force an update.
Two $state variants handle edge cases:
$state.rawcreates shallow state. It is not made deeply reactive and only updates when you replace it (reassign the whole value), not when you mutate it. This avoids the cost of proxying large objects or arrays you never mutate.7$state.snapshotreturns a detached, plain (non-proxy) copy of a deep$stateobject. Mutating the snapshot does not affect the original, and reading it does not register a dependency — useful when passing data to a non-Svelte library that chokes on proxies.7
You can also mark class fields as $state, which is how you build reusable reactive classes.6
How does $derived work? ($derived vs $:)
$derived declares a value computed from other reactive state, recomputed automatically when its dependencies change. It replaces Svelte 4's $: computed declarations.6
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>
The expression inside $derived(...) must be free of side effects — Svelte disallows state changes like count++ inside a derived.6 For logic that doesn't fit in one expression, use $derived.by, which takes a function:
<script>
let numbers = $state([1, 2, 3]);
let total = $derived.by(() => {
let sum = 0;
for (const n of numbers) sum += n;
return sum;
});
</script>
$derived(expr) is exactly equivalent to $derived.by(() => expr).6 Three behaviors are worth knowing because they explain why deriveds are cheap:
- Lazy and memoized. Svelte uses push-pull reactivity: when state changes, dependents are notified (push), but a derived is not recalculated until it is actually read (pull). A derived that is never accessed never runs.6
- Referential short-circuiting. If a recomputed derived is referentially identical to its previous value, downstream updates are skipped.6
- Overridable. Since Svelte 5.25, you can temporarily reassign a derived (unless it's
const) — handy for optimistic UI where you show an immediate change before the server confirms it.6
When should you use $effect — and when not to?
$effect runs a function when its reactive dependencies change. It is for side effects: calling third-party libraries, drawing on a <canvas>, making network requests, or analytics. Effects run in the browser only — never during server-side rendering.3
<script>
let theme = $state('dark');
$effect(() => {
document.documentElement.dataset.theme = theme;
});
</script>
Key behaviors from the docs:3
- Effects run after the component mounts to the DOM, then in a microtask after state changes. Re-runs are batched and happen after DOM updates apply.
- An effect tracks reactive values (
$state,$derived,$props) read synchronously in its body. Values read asynchronously — afterawaitor insidesetTimeout— are not tracked. - An effect can return a teardown function, which runs immediately before the effect re-runs and when the component is destroyed (perfect for
clearInterval):
<script>
let ms = $state(1000);
let count = $state(0);
$effect(() => {
const id = setInterval(() => count++, ms);
return () => clearInterval(id); // teardown
});
</script>
Now the part worth emphasizing. The official guidance is that $effect is an escape hatch, not a default tool. In particular, do not use it to synchronise state.3 This is the anti-pattern:
<script>
let count = $state(0);
let doubled = $state();
// ❌ don't do this
$effect(() => {
doubled = count * 2;
});
</script>
Replace it with a derived, which is simpler, lazier, and avoids extra render passes:
<script>
let count = $state(0);
let doubled = $derived(count * 2); // ✅ do this
</script>
If you ever must write to $state inside an effect and hit an infinite loop because you read and write the same state, wrap the read in untrack (imported from svelte) so it isn't registered as a dependency.3 For linking two inputs together, prefer function bindings or oninput callbacks over effects.3
Svelte also exposes advanced sub-runes: $effect.pre (runs before the DOM updates, e.g. autoscroll via tick()), $effect.tracking() (whether you're in a tracking context), $effect.pending() (count of pending promises when using await in components — part of the experimental async feature you opt into with experimental.async), and $effect.root() (a manually managed, non-auto-cleaning effect scope).3
$derived vs $effect: which rune should you use?
Use this decision rule: if you are producing a value, use $derived; if you are performing an action, use $effect. The table below maps common tasks.
| You want to… | Use | Why |
|---|---|---|
| Compute a value from state | $derived | Pure, lazy, memoized; recomputed only when read |
| Multi-statement computation | $derived.by | Same as $derived but with a function body |
| Mirror one state value into another | $derived | Never $effect — the docs single this out as an anti-pattern3 |
| Call an API / log / touch the DOM | $effect | True side effect, browser-only |
| Set up and tear down a subscription/timer | $effect + teardown | Return a cleanup function |
| Run code before the DOM updates | $effect.pre | Runs before DOM updates |
A simple gut check: if your effect body ends in an assignment to another $state, you almost certainly want $derived instead.
How do $props and $bindable work?
$props reads a component's inputs via destructuring, with default values, replacing Svelte 4's export let:8
<!-- Greeting.svelte -->
<script>
let { name = 'world', count = 0 } = $props();
</script>
<p>Hello {name} ({count})</p>
<!-- TextInput.svelte -->
<script>
let { value = $bindable('') } = $props();
</script>
<input bind:value />
<!-- Parent.svelte -->
<script>
import TextInput from './TextInput.svelte';
let name = $state('');
</script>
<TextInput bind:value={name} />
<p>{name}</p>
Do Svelte stores still work in Svelte 5?
Yes. Stores from svelte/store — writable, readable, derived, and the $store auto-subscription syntax — still work in Svelte 5 and are not deprecated.4 The official recommendation is to prefer runes for most component and shared state, but stores remain useful, especially for existing code and for cases like RxJS-style external subscriptions. Because both coexist, you never need a big-bang rewrite.
A common pattern for shared reactive state in Svelte 5 is to move it into a .svelte.js module using $state, then export accessors:
// counter.svelte.js
let count = $state(0);
export function getCount() {
return count;
}
export function increment() {
count++;
}
This works because runes are valid outside components in .svelte.js/.svelte.ts files.4
How do you migrate Svelte 4 reactivity to runes?
Migration is mechanical, and Svelte ships an automated migration script, but the core mappings are:4
- Reactive
let x = 0→let x = $state(0) $: doubled = x * 2(computed) →let doubled = $derived(x * 2)$: { sideEffect(x); }(side effect) →$effect(() => { sideEffect(x); })export let name→let { name } = $props()export let valuewithbind:→let { value = $bindable() } = $props()
The one judgment call is the old $: label, which Svelte 4 used for both computed values and side effects. Split it: anything that produces a value becomes $derived; anything that performs an action becomes $effect. When in doubt, choose $derived — it's the cheaper, safer default.
Bottom line and next steps
Svelte 5 runes turn reactivity from compiler magic into three explicit tools: $state for values, $derived for computed values, and $effect for side effects — with $props/$bindable for component inputs.58 A simple rule that avoids a whole class of bugs is to reach for $derived before $effect, and to treat $effect as an escape hatch for things like network requests and DOM work rather than a way to keep state in sync.3
If you're new to the framework, start with the fundamentals in our guide to building fast apps with Svelte, then put runes to work in a real feature with type-safe forms using Astro Actions. For broader frontend state and styling patterns, Tailwind v4 dark mode pairs well with a $state-driven theme toggle like the $effect example above.
Footnotes
-
Svelte — "Svelte 5 is alive" (stable release, October 19, 2024). https://svelte.dev/blog/svelte-5-is-alive ↩
-
Svelte — "What's new in Svelte: June 2026" (current 5.56.x line; language-tools TypeScript 6.0 support). https://svelte.dev/blog/whats-new-in-svelte-june-2026 ↩
-
Svelte Docs — "$effect" (lifecycle, dependency tracking, teardown, $effect.pre/tracking/pending/root, "When not to use $effect", untrack). https://svelte.dev/docs/svelte/$effect ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11
-
Svelte Docs — "What are runes?" and migration/stores references. https://svelte.dev/docs/svelte/what-are-runes ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Svelte Docs — "$state" (rune list in left navigation; reactive state). https://svelte.dev/docs/svelte/$state ↩ ↩2 ↩3 ↩4
-
Svelte Docs — "$derived" (derived state, $derived.by, push-pull reactivity, overriding since 5.25, deep-proxy note). https://svelte.dev/docs/svelte/$derived ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10
-
Svelte Docs — "$state" ($state.raw shallow state; $state.snapshot detached copy). https://svelte.dev/docs/svelte/$state ↩ ↩2
-
Svelte Docs — "$props" (component props via destructuring with defaults). https://svelte.dev/docs/svelte/$props ↩ ↩2