Svelte 5 Runes

Overview: This tutorial explains Svelte 5's reactivity primitives (runes), how to use them in components and plain modules, and provides practical examples and migration tips.

What are runes

Runes are explicit reactivity primitives introduced in Svelte 5. They make reactive state, derived values, and side effects explicit to the compiler and to developers. Runes are typically prefixed with $ and include primitives such as $signal, $computed (or $derived), and $effect.


Core primitives and when to use them

RunePurposeWhen to use
$signalMutable reactive state containerLocal component state or shared reactive state passed between modules
$computedPure derived values from signalsMemoized transforms and pure logic
$effectRun side effects when dependencies changeLogging, network requests, DOM interactions
$derivedAlternative derived API (usage restrictions may apply)When you need a declaration-style derived value
Key idea: Runes make dependencies explicit so the compiler can generate efficient update code and so your reactive logic works the same inside modules and components.

Basic examples

1. Signal: reactive state

Declare a signal inside a component to hold mutable state. Use it directly in markup and update it like a normal variable.

<script lang="ts">
  $signal count = 0;

  function increment() {
    count += 1;
  }
</script>

<button on:click={increment}>Clicks: {count}</button>

2. Computed: derived value

Use $computed for pure derived values. The function runs when dependencies change and is memoized by the compiler.

<script lang="ts">
  $signal count = 0;

  $computed double = () => count * 2;
</script>

<p>Count: {count} — Double: {double}</p>

3. Effect: side effects

Use $effect to run side effects when dependencies change. Keep effects focused and avoid mixing many responsibilities in one effect.

<script lang="ts">
  $signal count = 0;

  $effect(() => {
    console.log('count changed to', count);
  });
</script>

Using runes in plain modules

One of the major benefits of runes is that they work in plain .js/.ts modules, not only inside .svelte files. This makes reactive logic portable and testable.

// store.ts
$signal sharedCount = 0;

export function incrementShared() {
  sharedCount += 1;
}

export { sharedCount };

Then import into a component:

<script lang="ts">
  import { sharedCount, incrementShared } from './store';

</script>

<button on:click={incrementShared}>Shared: {sharedCount}</button>

Note: Because runes are compiler-aware, bundlers and the Svelte compiler will generate efficient subscriptions and updates for these module-level signals.


Advanced patterns

Derived stores and async computed values

For derived values that involve async work, keep the async logic inside an effect and expose a signal for the result.

// asyncExample.ts
$signal query = '';
$signal results = null;

$effect(async () => {
  if (!query) {
    results = null;
    return;
  }
  const q = query; // capture
  const res = await fetch('/api/search?q=' + encodeURIComponent(q));
  if (q === query) { // avoid race
    results = await res.json();
  }
});

export { query, results };

Composing computed values

Computed values can depend on other computed values. Keep them pure and small for testability.

$signal a = 2;
$signal b = 3;

$computed sum = () => a + b;
$computed doubledSum = () => sum * 2;

SSR and lifecycle considerations

Server-side rendering: When using runes with SSR, avoid effects that perform browser-only operations (DOM, localStorage, window). Guard them with environment checks or run them only on the client.

$effect(() => {
  if (typeof window === 'undefined') return;
  // browser-only side effect
});

Lifecycle: Runes integrate with Svelte's lifecycle. Effects run after initial render and on dependency changes. If you need cleanup, return a function from the effect.

$effect(() => {
  const id = setInterval(() => {
    // periodic work
  }, 1000);

  return () => clearInterval(id);
});

Migration tips from Svelte 3 / 4

  1. Start small: Convert a few components to runes where portability or clearer dependency graphs matter.
  2. Prefer $computed for pure transforms and $effect for I/O.
  3. Test bindings and SSR after conversion—runes change scheduling and compilation details.
  4. Avoid overusing $effect; split responsibilities into multiple effects or computed values.

Best practices and pitfalls


Full example: small counter app with shared store

// store.ts
$signal count = 0;

export function increment() {
  count += 1;
}

export function decrement() {
  count -= 1;
}

export { count };
<!-- Counter.svelte -->
<script lang="ts">
  import { count, increment, decrement } from './store';
</script>

<div>
  <button on:click={decrement}>-</button>
  <strong>{count}</strong>
  <button on:click={increment}>+</button>
</div>

Troubleshooting checklist