Svelte 5 Runes: $state, $derived, $effect Reactivity

Photo by Chris Ried on Unsplash
Runes are symbols with a dollar-sign prefix that you use in .svelte, .svelte.js, and .svelte.ts files to control the Svelte compiler. They look like function calls but are keywords, so you do not import them, cannot store them in variables, and can only use them in specific positions. The core set is $state, $derived, $effect, and $props.
In Svelte 4 the compiler invalidated an entire object when one property changed, and the dollar-colon syntax conflated derived state with side effects. Runes make reactivity explicit and let the compiler wire up fine-grained, signals-based updates, closing the performance gap with other signal-based frameworks. Svelte 5 was released stable in October 2024.
$derived computes a value from other reactive state and must be free of side effects, recomputing automatically when its dependencies change. $effect runs side effects after the component mounts and re-runs when the state it reads changes. The docs advise against updating state inside effects, since that leads to convoluted code and infinite update loops; use $derived for computed values instead.
Yes. Because runes are a compiler feature rather than a component feature, they work in .svelte.js and .svelte.ts modules. This universal reactivity lets you write reusable reactive logic once and import it anywhere, exposing state through getters, which for most shared logic replaces the older writable stores API.
Run npx sv migrate svelte-5. It bumps core dependencies, converts implicit reactive let to $state, rewrites dollar-colon statements to $derived or $effect, migrates event attributes like on:click to onclick, and turns slots into snippets and render tags. Some manual cleanup remains, so review the diff on a branch.

Photo by Chris Ried on Unsplash
Key Takeaway
Svelte 5, released October 2024, replaces implicit compiler-driven reactivity with runes: explicit symbols like $state, $derived, $effect, and $props that mark reactive values. Under the hood, Svelte uses signals for fine-grained updates, and runes work in .svelte.js and .svelte.ts modules, letting developers share reactive logic outside components.
For years, Svelte's headline trick was that reactivity looked like nothing at all. You wrote a plain let count = 0, reassigned it, and the UI updated. A top-level let in a component was implicitly reactive, and a dollar-colon label turned any statement into a re-running derivation. It felt magical the first time, and confusing the tenth, because whether a variable was reactive depended on where it lived rather than on anything you could see in the line itself.
Svelte 5, released stable in October 2024, keeps the compiled-away output but throws out the implicit rules. Reactivity is now declared with runes — explicit symbols you can read at a glance. I have shipped enough Svelte components to have been bitten by the old model, so this trade of a little more typing for a lot more clarity is one I welcome.
The official docs define runes as symbols you use in .svelte and .svelte.js or .svelte.ts files to control the Svelte compiler. They use a dollar-sign prefix and look like function calls, but they are not functions: you do not import them, you cannot store them in a variable or pass them as arguments, and they only work in specific positions. They are keywords in the Svelte language, validated by the compiler.
The core set is small. $state declares reactive state, $derived computes values from other state, $effect runs side effects when dependencies change, and $props declares the inputs a component receives from its parent. There are a few more — $bindable, $inspect, $host — but those four carry nearly every component you will write.
$state is the starting point. You write let count = $state(0) and, as the docs put it, count is just a number, rather than an object or a function, and you can update it like you would update any other variable. For arrays and plain objects, $state returns a deeply reactive proxy, so mutating a nested field or calling push is tracked. $derived takes an expression that must be free of side effects and recomputes whenever its dependencies change; $effect runs after the component mounts and re-runs in a microtask after the state it read changes.
<script>
// $state declares reactive state — count is just a number
let count = $state(0);
// $derived recomputes automatically when count changes
let doubled = $derived(count * 2);
// $effect runs after mount and re-runs when its deps change
$effect(() => {
console.log('count is now', count);
});
</script>
<button onclick={() => count++}>
clicks: {count} (doubled: {doubled})
</button>The example above shows all three cooperating. count is reactive state, doubled is derived from it and never assigned by hand, and the effect logs whenever count moves. Notice there is no subscribe call and no dependency array — Svelte tracks which state each derivation and effect reads, and re-runs exactly those that depended on what changed.
Prefer $derived over $effect for anything that computes a value. The docs warn that you should generally not update state inside effects, because it makes code convoluted and often leads to never-ending update cycles. If you find yourself writing an effect that sets another piece of state, it is almost always a derived value in disguise.
The move was not fashion. The Svelte team explained that in Svelte 4, if you change a single property of a reactive object, the entire object is invalidated, because that is all the compiler can realistically do. Meanwhile other frameworks adopted fine-grained reactivity based on signals and leapfrogged Svelte's performance. The old dollar-colon statement also conflated two concepts — derived state and side effects — that really should be kept separate. Runes split those apart and let the compiler wire up fine-grained, signal-based updates instead of invalidating whole objects.
Runes are position-specific keywords, not values. You cannot do const r = $state or pass $derived to a helper function — the compiler rejects it. If a lint rule or teammate suggests extracting a rune into a shared utility, the right move is a .svelte.js module that returns reactive getters, not a variable holding the rune itself.
The change that quietly matters most is universal reactivity. Because runes are a compiler feature rather than a component feature, they work in .svelte.js and .svelte.ts modules too. That means you can write reusable reactive logic once and import it anywhere, using a single mechanism instead of the old stores API.
// counter.svelte.js — universal reactivity outside components
export function createCounter() {
let count = $state(0);
return {
get count() { return count; },
increment: () => count += 1,
reset: () => count = 0,
};
}
// any .svelte component can now import and use this:
// import { createCounter } from './counter.svelte.js';
// const counter = createCounter();In Svelte 4, sharing reactive state across components meant reaching for writable stores and their subscribe or dollar-prefix access. With runes you can express the same thing as ordinary JavaScript that happens to be reactive, exposing state through getters. Stores still exist and still have their place, but for most shared logic a .svelte.js module is now the simpler default.
The mental model maps cleanly, and there is an automated path. Running the migration command upgrades core dependencies and rewrites most of the old syntax for you, though some manual cleanup remains. The key substitutions are:
| Concern | Svelte 4 | Svelte 5 runes |
|---|---|---|
| Declaring state | Implicit — any top-level let | Explicit — let x = $state(...) |
| Derived values | Dollar-colon reactive statement | $derived expression |
| Side effects | Also dollar-colon — same syntax as derivations | $effect — kept separate from derivations |
| Component props | export let per property | One $props destructuring |
| Reactivity outside components | Writable and readable stores | Runes in .svelte.js and .svelte.ts modules |
My advice is to start a new feature in runes rather than converting a whole app on day one. The explicitness pays off fastest in code you are actively changing, where knowing at a glance which values are reactive removes a whole class of debugging. Run the migration script on a branch, read its diff, and treat the leftover manual fixes as a chance to learn where the old magic was hiding.