Svelte 5's Runes: A Genuine Rethink of Reactivity, Not Just New Syntax

By James Nguyen Updated September 24, 2026
Svelte 5's Runes: A Genuine Rethink of Reactivity, Not Just New Syntax

Migrating a genuinely working Svelte 4 application to Svelte 5's rune-based reactivity system felt like unnecessary busywork at first glance, syntax churn for its own sake. Actually working through the migration revealed real, substantive architectural improvements I hadn't appreciated just reading through the release notes beforehand.

What Runes Actually Change

Svelte 4's reactivity relied on the compiler inferring reactive behavior from plain variable assignments and a special $: syntax, magic that worked well until it didn't, and debugging why something wasn't reactive often meant understanding compiler internals rather than reading the code directly. Runes make reactivity explicit, a variable is reactive because you wrote $state, not because the compiler guessed based on how it was used.

// Svelte 4
let count = 0;
$: doubled = count * 2;

// Svelte 5, runes
let count = $state(0);
let doubled = $derived(count * 2);

Reactivity Outside Component Files, a Genuine New Capability

This is the change that mattered most for our actual codebase. Svelte 4's reactivity was trapped inside .svelte component files, meaning any shared reactive logic had to route through Svelte's store system, a real, separate abstraction. Runes work in plain .svelte.ts files too, letting us write reusable reactive logic as genuine TypeScript modules rather than always reaching for stores.

// counter.svelte.ts
export function createCounter(initial = 0) {
  let count = $state(initial);
  return {
    get value() { return count; },
    increment() { count++; },
  };
}

$effect Replacing Reactive Statements for Side Effects

Svelte 4's $: syntax conflated two genuinely different concepts, derived values and side effects, into one syntax, which was a real, recurring source of confusion. Runes split these explicitly into $derived for computed values and $effect for side effects, and that separation alone clarified intent in code that had previously required reading carefully to understand which behavior a given $: statement was actually for.

$effect(() => {
  console.log("count changed to", count);
  localStorage.setItem("count", String(count));
});

Props Handling, Genuinely Simpler

$props() replaces Svelte 4's export let syntax for component props, and the new syntax handles destructuring, defaults, and rest props more cleanly than the old approach, which had accumulated some genuinely awkward edge cases around prop reactivity that runes resolve more predictably.

let { title, count = 0, ...rest } = $props();

The Migration Itself, Genuinely Gradual

Svelte 5 maintains backward compatibility with Svelte 4 syntax, meaning we converted components incrementally rather than needing a disruptive, all-at-once rewrite, a real practical relief given our earlier, painful experience with a forced-all-at-once JavaScript-to-TypeScript migration on a different project.

SvelteKit 2 Integration

SvelteKit 2 shipping alongside Svelte 5 with first-class rune support meant our routing and data loading patterns needed only modest adjustment, mainly switching from the deprecated $app/stores import to $app/state for accessing page and navigation data reactively.

// old: import { page } from "$app/stores"; then $page.url.pathname
import { page } from "$app/state";
page.url.pathname // reactive, no $ prefix needed

Where the Migration Surfaced Real Bugs

Converting a component with a genuinely tangled set of interdependent $: statements, runes' explicit dependency tracking flagged a circular reactive dependency that Svelte 4 had apparently been resolving through undefined, order-dependent behavior we'd never actually noticed causing a problem, until making the dependencies explicit forced us to actually fix the underlying design.

My Honest Take

This isn't cosmetic syntax churn dressed up as a major version bump. The explicit reactivity model, and specifically the ability to write reactive logic outside component files, are genuine architectural improvements that changed how we structure shared logic across the application, not just how individual components are written.

Daniel Justin

About the Author

James Nguyen is a full-stack programmer with more than ten years of experience engineering software systems. Specializing in the Node.js and Python ecosystems, he focuses on backend architecture, API design, and clean data integration. Follow me on YouTube and Instagram.

More Articles