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.
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.
| Rune | Purpose | When to use |
|---|---|---|
| $signal | Mutable reactive state container | Local component state or shared reactive state passed between modules |
| $computed | Pure derived values from signals | Memoized transforms and pure logic |
| $effect | Run side effects when dependencies change | Logging, network requests, DOM interactions |
| $derived | Alternative derived API (usage restrictions may apply) | When you need a declaration-style derived value |
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>
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>
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>
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.
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 };
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;
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);
});
// 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>
$signal and not a plain variable.typeof window !== 'undefined'.