Pavan Rangani

HomeBlogSvelte 5 Runes and Reactivity: Complete Guide 2026

Svelte 5 Runes and Reactivity: Complete Guide 2026

By Pavan Rangani · March 8, 2026 · Web Development

Svelte 5 Runes and Reactivity: Complete Guide 2026

Svelte Runes Reactivity: A New Mental Model

Svelte runes reactivity replaces Svelte’s compiler-magic reactivity with an explicit signal-based system using special $-prefixed functions called runes. Therefore, reactivity becomes predictable, composable, and works outside .svelte files in plain JavaScript modules. As a result, Svelte 5 eliminates the implicit reactivity gotchas that confused developers in previous versions. Previously, a single `let` declaration at the top of a component was magically reactive while the same statement inside a helper function was not. Now, that ambiguity disappears because the rune itself marks the boundary between ordinary and reactive state.

Understanding $state and $derived

The $state rune creates reactive values that trigger UI updates when modified, similar to signals in other frameworks. Moreover, $derived computes values that automatically update when their dependencies change, replacing the $: label syntax. Consequently, the reactivity contract becomes explicit — you always know which values are reactive because they were created with a rune.

Deep reactivity tracks nested object and array mutations automatically without requiring immutable update patterns. So you can push to an array or set a nested property directly, and the UI still updates. Under the hood, Svelte wraps objects and arrays in a Proxy that intercepts reads and writes. Furthermore, $state.raw opts out of deep reactivity for large data structures where the proxy overhead matters. For instance, a 50,000-row dataset that you only ever replace wholesale should use `$state.raw` to skip per-property tracking. When you do need a computed value with side effects or a multi-step calculation, $derived.by accepts a function body instead of a single expression, which keeps complex derivations readable.

Svelte runes reactivity development
Runes make reactivity explicit and composable

Runes Reactivity in Practice: Effects and Cleanup

Effects run automatically when reactive dependencies change, replacing beforeUpdate and afterUpdate lifecycle hooks. Additionally, $effect.pre runs before DOM updates while $effect runs after, covering all lifecycle timing needs. For example, a search component can debounce API calls reactively without manual subscription management. Importantly, effects track dependencies dynamically — only the reactive values actually read during a given run are subscribed. Consequently, a branch that never executes will not re-trigger the effect, which avoids a whole class of stale-closure bugs.

// Svelte 5 runes-based component
<script>
  let count = $state(0);
  let doubled = $derived(count * 2);
  let history = $state([]);

  // Effect runs when count changes
  $effect(() => {
    history = [...history, count];
    console.log(`Count changed to ${count}, doubled: ${doubled}`);
  });

  // Extracted reactive logic (works in .svelte.js files too)
  function createTimer(interval) {
    let elapsed = $state(0);
    let running = $state(false);

    $effect(() => {
      if (!running) return;
      const id = setInterval(() => elapsed++, interval);
      return () => clearInterval(id);  // cleanup
    });

    return {
      get elapsed() { return elapsed; },
      get running() { return running; },
      start: () => running = true,
      stop: () => running = false,
      reset: () => elapsed = 0,
    };
  }

  const timer = createTimer(1000);
</script>

<button onclick={() => count++}>Count: {count}</button>
<p>Doubled: {doubled}</p>
<p>Timer: {timer.elapsed}s</p>

Notice the getter pattern in the returned object. Because runes are not values you can copy but references the compiler tracks, you cannot simply return `{ elapsed }` — that would capture the value at one instant and lose reactivity. Instead, exposing a getter keeps the consumer subscribed to the live signal. Reactive classes follow the same principle: declaring $state in class fields gives object-oriented patterns automatic reactivity. Therefore, complex state management integrates naturally with TypeScript class-based architectures, and the same class works identically whether instantiated in a component or in a plain module.

Sharing State Across Files with .svelte.js

One of the most practical wins is that runes work in `.svelte.js` and `.svelte.ts` modules, not just components. As a result, you can build a shared store as plain code instead of reaching for a separate state library. The pattern below creates a global theme store that any component can import and mutate, with the UI updating everywhere automatically.

// theme.svelte.js — shared reactive module
let theme = $state('light');
let fontSize = $state(16);

export const settings = {
  get theme() { return theme; },
  get fontSize() { return fontSize; },
  toggle() { theme = theme === 'light' ? 'dark' : 'light'; },
  bumpFont(delta) { fontSize = Math.max(12, fontSize + delta); },
};

// derived values also export cleanly
export const cssVars = $derived.by(() => ({
  '--fg': theme === 'dark' ? '#eee' : '#222',
  '--bg': theme === 'dark' ? '#111' : '#fff',
}));

However, be deliberate about module-level state. Because the module is a singleton, every importer shares the same instance — which is exactly what you want for app-wide settings but wrong for per-request state in SSR. In server-side rendering, module-scoped $state leaks between requests, so keep request-specific data in component scope or in context. The docs recommend `setContext`/`getContext` for state that must be unique per component tree.

Migration from Svelte 4

The svelte-migrate tool automates most transformations from Svelte 4 reactive declarations to runes. However, some patterns like reactive assignments to exported props require manual review. In contrast to Svelte 4’s implicit reactivity, runes require explicit opt-in but eliminate surprising behavior. The most common manual fixes involve the old `export let` props, which become `$props()` destructuring, and `$:` blocks that mixed derivation with side effects — those must be split into a clean $derived plus a separate $effect. Teams migrating large codebases typically do it incrementally, since Svelte 5 runs legacy components and runes components side by side during the transition. For a broader look at modern build tooling that pairs well with this upgrade, see the related Vite 6 features guide.

Frontend framework migration
Automated migration tools ease the Svelte 4 to 5 transition

Performance Benefits and When to Reach for $state.raw

Fine-grained reactivity updates only the specific DOM nodes affected by state changes rather than re-rendering entire component trees. Additionally, Svelte 5’s compiler generates more efficient update code that reduces bundle size compared to Svelte 4. Specifically, benchmarks show 20-40% faster updates for complex interactive applications, and the runtime no longer needs to diff whole component outputs because each signal knows precisely which DOM bindings depend on it.

That said, deep reactivity is not free. The Proxy wrapping that makes nested mutation just work adds a small per-access cost, which is negligible for typical UI state but measurable when you iterate over tens of thousands of items in a tight loop. Therefore, when you hold large immutable datasets — parsed CSV, geometry buffers, or a cached API response you only ever swap — use `$state.raw`. It stores the value as-is, skips proxying entirely, and still triggers updates on reassignment.

// Heavy dataset: skip per-property proxying
let rows = $state.raw([]);

async function load() {
  const res = await fetch('/api/report');
  rows = await res.json();   // reassignment IS reactive
}

// rows[0].value = 5  // NOT reactive with .raw — reassign instead
function update(i, value) {
  rows = rows.map((r, idx) => idx === i ? { ...r, value } : r);
}

When NOT to Reach for Runes (and Honest Trade-offs)

Runes are a clear win, but they are not a reason to rewrite a working Svelte 4 app overnight. If a project is in maintenance mode and ships fine, the migration cost may outweigh the benefit. Moreover, the explicitness that makes runes predictable also makes them slightly more verbose — trivial components that worked with a plain `let` now carry `$state` ceremony. Another sharp edge: you cannot destructure a $state object and keep reactivity, since destructuring copies the value out of the proxy. Likewise, passing a reactive value into a non-reactive function snapshots it. Once you internalize that runes are references rather than plain values, these rules feel natural, but they trip up newcomers coming from React’s render-driven model.

Web performance and optimization
Fine-grained updates deliver superior runtime performance

Related Reading:

Further Resources:

In conclusion, Svelte runes reactivity provides a predictable and composable reactivity system that scales from simple components to complex applications. Because reactivity is now explicit, portable across files, and tuned with escape hatches like $state.raw, you gain both clarity and control. Therefore, migrate to Svelte 5 when you start new work to benefit from explicit reactivity, better performance, and dramatically improved code reuse.

← Back to all articles