Svelte 5 Runes: Reactivity Out in the Open

Svelte 5 with runes shipped last October, and a year on it’s settled. The plain version of what runes do: they take the thing Svelte used to handle by guessing at compile time which variables are reactive and make you spell it out yourself. The $-prefixed symbols — $state, $derived, $props, $effect — aren’t functions; they’re compiler-level keywords. No imports, and they work in both .svelte and .svelte.ts files. If you’ve written Svelte 3/4, this is a shift worth getting used to.

From “the compiler guesses” to “you say it plainly”

Svelte 4 reactivity leaned on let declarations and $: labels, worked out by static analysis at compile time. The upside was clean code; the downside was that the magic lived inside the compiler. Once a component grew and the logic wound around itself, you’d start guessing which variable actually triggered an update, and in what order the $: blocks ran. Compiling is not the same as understanding how your data flows.

Runes pull that layer into the open. let count = $state(0) declares a reactive value; count is still an ordinary number, you bump it with count++ like anything else, no separate setter API. Computed values go through $derived:

<script>
  let count = $state(0);
  let doubled = $derived(count * 2);
</script>

<button onclick={() => count++}>{doubled}</button>

Anything read inside $derived becomes its dependency; when a source changes the derived is marked dirty and recomputed the next time it’s read. The difference from Svelte 4’s $: is that reactivity stops being a side-effect of how you named a variable and becomes a primitive you deliberately apply. One less layer of guessing, a bit more predictability.

I like the direction, but I’m not going to pretend it’s free. What you pay for the explicitness is a handful of rules to keep in mind. $state on an object or array gives you a deeply reactive proxy — push and property writes both trigger updates — but the moment you destructure a value out, it stops being reactive, because that’s a snapshot JavaScript takes at the point of destructuring. Class instances aren’t proxied, so you mark each field with $state individually. None of this is hard, but you have to know it. The trick is to reason about how state flows, not to memorise these as API trivia. Memorise the API and you’ll trip over an edge case sooner or later.

Against React/Vue, one fewer layer of abstraction

For anyone with DOM intuition, Svelte’s mental model has always read more smoothly: you write markup plus state, and the compiler emits code that touches the DOM directly, with no virtual DOM in between. React’s re-render model asks you to remember at all times that the whole function component reruns, which is why useMemo, useCallback, and dependency arrays exist — a whole toolkit built to suppress recomputation. Most of that isn’t solving a business problem; it’s wrestling with the framework’s execution model.

Svelte 5’s push-pull reactivity takes the other road: when state changes, whatever depends on it is notified immediately (the push), but a $derived isn’t recomputed until something actually reads it (the pull). And if the new value is referentially identical to the old one, downstream updates are skipped outright. There’s no sorcery here — it’s just being explicit about who depends on whom and when a recompute is due. Against React’s manually-annotated dependencies, Svelte hands the bookkeeping back to the compiler, but the rules of that bookkeeping are ones you can see.

Vue’s ref/reactive/computed are conceptually close to runes, honestly — explicit reactive primitives, all of them. The difference is Svelte has no .value wrapper and no virtual-DOM diff. You wanted one fewer layer of abstraction; one fewer is what Svelte gives you.

The real trade-off: module state retires stores

The most practical win from runes is that sharing state across components got clean. Before, you either reached for Svelte stores — writable, subscriptions, the $store auto-subscription syntax — or rolled something yourself. Now you write reactive module state directly in a .svelte.ts file.

Take a shopping cart. The requirement is dull: share it across pages, keep it after a refresh. I used to open a store, wire subscribe into localStorage, then handle reading it back on init. A single .svelte.ts module absorbs all of that:

// cart.svelte.ts
function createCart() {
  let items = $state<Item[]>(load());
  $effect.root(() => {
    $effect(() => localStorage.setItem('cart', JSON.stringify(items)));
  });
  return {
    get items() { return items; },
    add: (i: Item) => { items.push(i); },
    clear: () => { items = []; },
  };
}
export const cart = createCart();

There’s a sharp edge here: you can’t just export let count = $state(0) and reassign it everywhere, because the compiler rewrites every reference to count, so cross-module reassignment won’t connect. So you either wrap it in an object exposed through getters, or hand out operations like add() / clear(). Know that one rule and the design falls out naturally.

The old stores aren’t gone — svelte/store is still there — but for new code, module state is usually more direct, with one fewer layer of $ auto-subscription sugar to explain to whoever maintains it next. When something can retire, let it.

For people who aren’t chasing the biggest ecosystem, who want their tooling to help them think rather than supply reassurance, Svelte 5 sits among the cleaner reference implementations in the current crop of frameworks. It doesn’t fold everything into framework abstraction; HTML stays the lead, and reactivity sits where you can see it. That’s enough.

Takeaways

  • Runes replace Svelte 4’s compiler-guessed reactivity with explicit primitives — $state / $derived / $props / $effect — trading magic for predictability.
  • The cost of explicitness is a few rules (deep proxy reactivity, destructuring drops reactivity, class fields marked individually); reason about state flow rather than memorising them.
  • Against React’s re-render model, Svelte has one fewer layer (no virtual DOM), which lowers the mental load for anyone with DOM intuition.
  • Shared state like a cart is clean as .svelte.ts module state plus localStorage — just remember you can’t directly export a reassigned $state.
  • For those not chasing ecosystem size, Svelte 5 is a relatively clean reference implementation among modern frameworks, with HTML still in the lead.

Sheng’s take, drafted with Claude · part of the 2026-06-13 blog renovation, paint still drying.