Svelte 5 Runes in Practice: $state, $derived, $effect, and Migration from Svelte 4
Why Svelte 5 Deserves Your Attention
Svelte 5 rewrote its reactivity system with Runes — moving from "compile-time dependency tracking" to "explicit, fine-grained, cross-file" reactive primitives. It fixes the Svelte 4 problem where let variables lost tracking inside closures, async code, and shared modules, while shipping a smaller runtime and better performance.
| Dimension | Svelte 4 | Svelte 5 (Runes) |
|---|---|---|
| State | let count = 0 |
let count = $state(0) |
| Derived | $: doubled = count * 2 |
let doubled = $derived(count * 2) |
| Side effect | $: console.log(count) |
$effect(() => console.log(count)) |
| Cross-module | needs stores | runes directly in .svelte.js |
| Props | export let |
let { x } = $props() |
Core Runes at a Glance
$state: declare reactive state
<script>
let count = $state(0);
let user = $state({ name: "Ada", age: 36 });
function inc() {
count++; // triggers update automatically
user.age++; // deep props are reactive too (proxy-based)
}
</script>
<button onclick={inc}>{count} - {user.age}</button>
Note:
$stateuses a deep proxy for objects and arrays, souser.age++is precisely tracked — no more "replace the whole object" pattern from Svelte 4.
$derived: declare derived values
<script>
let price = $state(100);
let qty = $state(2);
let total = $derived(price * qty); // recomputed only when deps change
let label = $derived.by(() => {
// wrap complex logic with .by
return price > 0 ? `$${price * qty}` : "invalid price";
});
</script>
<p>Total: {total} / {label}</p>
$effect: declare side effects
<script>
let count = $state(0);
$effect(() => {
// reading count creates the dependency; re-runs when it changes
document.title = `Count: ${count}`;
});
</script>
$effect runs only in the browser — ideal for DOM sync, third-party bindings, and subscription cleanup. Return a function to clean up:
<script>
let el = $state();
$effect(() => {
if (!el) return;
const obs = new ResizeObserver(() => {/* ... */});
obs.observe(el);
return () => obs.disconnect(); // auto-cleanup
});
</script>
<div bind:this={el}></div>
Props and Two-Way Binding
$props replaces export let
<!-- Child.svelte -->
<script>
let { title, count = 0, onchange } = $props();
</script>
<h2>{title}: {count}</h2>
$bindable for bindable props
<!-- Child.svelte -->
<script>
let { value = $bindable("") } = $props();
</script>
<input bind:value />
<!-- Parent.svelte -->
<script>
let name = $state("");
</script>
<Child bind:value={name} />
<p>Parent: {name}</p>
Reusing Runes in .svelte.js Modules
A major Svelte 5 win: runes are no longer confined to .svelte files — declare reactive state in .svelte.js / .svelte.ts for true cross-component reuse.
// counter.svelte.js
export const createCounter = () => {
let count = $state(0);
const inc = () => count++;
const double = $derived(count * 2);
return { get count() { return count; }, get double() { return double; }, inc };
};
<script>
import { createCounter } from "./counter.svelte.js";
const c = createCounter();
</script>
<button onclick={c.inc}>{c.count} / {c.double}</button>
For component state with complex config (theme colors, contrast thresholds), organize the config object with the JSON Formatter tool first, then wrap it as a reactive store with runes.
Lifecycle Changes
| Svelte 4 | Svelte 5 | Notes |
|---|---|---|
onMount |
onMount still works |
once on mount |
onDestroy |
$effect cleanup return |
replaces destroy cleanup |
$: side effect |
$effect |
unified reactive side-effect entry |
beforeUpdate/afterUpdate |
$effect.pre / $effect |
finer control |
<script>
import { onMount } from "svelte";
let ready = $state(false);
onMount(() => { ready = true; }); // mount logic still works
</script>
Smooth Migration from Svelte 4
Svelte 5 supports most Svelte 4 syntax (legacy mode), but migrate step by step:
- Upgrade deps:
npm i svelte@5, thennpx sv migrate svelte-5. - Migrate state first: change
let x =to$state(), especially variables used in closures/async. - Then derived:
$: y = f(x)→let y = $derived(f(x)). - Then effects:
$: sideEffect(x)→$effect(() => sideEffect(x)). - Finally props:
export let→$props(), add$bindablewhere two-way is needed.
<!-- Before (Svelte 4) -->
<script>
export let value = 0;
$: doubled = value * 2;
$: console.log(doubled);
</script>
<!-- After (Svelte 5) -->
<script>
let { value = 0 } = $props();
let doubled = $derived(value * 2);
$effect(() => console.log(doubled));
</script>
For visual config like colors and contrast, verify theme accessibility (WCAG) with the Color Converter and Color Contrast tools.
FAQ
Q1: Can runes be used in a plain .js file?
No. They must be in .svelte, .svelte.js, or .svelte.ts files so the compiler recognizes them.
Q2: Do non-reactive variable changes trigger $effect?
No. $effect only tracks reactive state read during execution. Reading a plain variable creates no dependency.
Q3: Can Svelte 4 code run directly on Svelte 5?
Mostly yes (legacy compatibility). But don't mix runes and legacy syntax within the same component — migrate component by component.
Q4: $derived vs $effect?
Use $derived for derived values; use $effect for doing things (DOM, subscriptions, logs).
Q5: Where does the performance gain come from?
Fine-grained tracking + compile-time optimization avoid virtual DOM diff overhead; state updates hit the minimal DOM nodes directly.
Recommended Tools
For Svelte 5 development, these ToolsKu tools help:
- JSON Formatter — Organize component config and state snapshots
- Color Converter — HEX/RGB/HSL conversion for theme state
- Color Contrast — Validate theme color WCAG accessibility
Svelte 5 runes are not syntactic sugar — they upgrade the mental model of reactivity: explicit state, precise dependencies, cross-file reuse. Master
$state / $derived / $effect / $propsand you hold the key to the next generation of Svelte.
Try these browser-local tools — no sign-up required →