Reactive State with Runes
Svelte 5's reactivity system is built on three runes: $state for mutable values, $derived for computed values, and $effect for side effects. Each has a specific role, and using the right one for the right job is the foundation of writing clean Svelte code.
$state: Mutable State
$state declares a reactive variable. When its value changes, anything that depends on it updates automatically.
<script>
let count = $state(0);
let name = $state('');
let items = $state<string[]>([]);
</script>
<button onclick={() => count++}>Count: {count}</button>
<input bind:value={name} placeholder="Your name">
<button onclick={() => items.push('new item')}>Add item ({items.length})</button>
The initial value passed to $state can be any type: primitive, object, array, or null. The variable behaves like a normal JavaScript variable after declaration. You read it, write to it, and pass it to functions. The compiler handles the reactivity wiring.
When to Use $state
Use $state when you have a value that:
- Changes over time due to user interaction, API responses, or timers.
- Needs to trigger UI updates when it changes.
- Is owned by this component (not derived from other values).
<script lang="ts">
let isOpen = $state(false);
let selectedTab = $state<'home' | 'settings' | 'profile'>('home');
let formData = $state({
email: '',
password: '',
rememberMe: false
});
</script>
<button onclick={() => isOpen = !isOpen}>
{isOpen ? 'Close' : 'Open'} Menu
</button>
Deep Reactivity: Arrays & Objects
Objects and arrays passed to $state are deeply reactive. Svelte wraps them in a proxy that tracks reads and writes at every level of nesting.
<script>
let todos = $state([
{ id: 1, text: 'Buy groceries', done: false },
{ id: 2, text: 'Walk the dog', done: true }
]);
</script>
<ul>
{#each todos as todo}
<li>
<input type="checkbox" bind:checked={todo.done}>
<span class:done={todo.done}>{todo.text}</span>
</li>
{/each}
</ul>
<button onclick={() => todos.push({ id: Date.now(), text: 'New todo', done: false })}>
Add Todo
</button>
<style>
.done { text-decoration: line-through; }
</style>
Mutations like todos.push(...), todo.done = true, or todos[0].text = 'Updated' all trigger reactivity. You do not need to create new arrays or spread objects. This is a significant difference from React, where you must treat state as immutable.
$state.raw: Opting Out of Deep Reactivity
For large datasets or objects you do not need to mutate in place, $state.raw creates state without the deep proxy. Updates only trigger when the entire value is reassigned.
<script>
// Large dataset: deep proxying would be wasteful
let rows = $state.raw(generateLargeDataset());
function refresh() {
// Must reassign entirely to trigger updates
rows = generateLargeDataset();
}
// This will NOT trigger an update:
// rows[0].name = 'changed';
// This WILL trigger an update:
// rows = [...rows];
</script>
Use $state.raw when:
- The data is large and you never mutate individual properties.
- The data comes from an API and you always replace it wholesale.
- You want to avoid the overhead of deep proxy wrapping.
$derived: Computed Values
$derived creates a value that is automatically computed from reactive dependencies. It is read-only and recalculates lazily when its dependencies change.
<script>
let items = $state([
{ name: 'Apple', price: 1.50, quantity: 3 },
{ name: 'Bread', price: 2.99, quantity: 1 },
{ name: 'Milk', price: 3.49, quantity: 2 }
]);
let totalItems = $derived(items.reduce((sum, item) => sum + item.quantity, 0));
let totalPrice = $derived(
items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
let formattedTotal = $derived(`$${totalPrice.toFixed(2)}`);
</script>
<p>{totalItems} items, total: {formattedTotal}</p>
When to Use $derived
Use $derived when:
- A value is computed entirely from other reactive values.
- You want the computation to stay in sync automatically.
- The value should be read-only (nobody should set it directly).
The rule is simple: if you can express a value as a function of other state, use $derived. Do not use $state and then manually keep it in sync with an $effect.
<script>
let search = $state('');
let items = $state(['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry']);
// Correct: derived from search and items
let filtered = $derived(
items.filter(item => item.toLowerCase().includes(search.toLowerCase()))
);
</script>
<input bind:value={search} placeholder="Filter...">
<ul>
{#each filtered as item}
<li>{item}</li>
{/each}
</ul>
$derived.by for Complex Computations
When the computation needs multiple statements, use $derived.by:
<script>
let transactions = $state([
{ amount: 100, type: 'credit' },
{ amount: -50, type: 'debit' },
{ amount: 200, type: 'credit' },
{ amount: -30, type: 'debit' }
]);
let summary = $derived.by(() => {
let credits = 0;
let debits = 0;
for (const tx of transactions) {
if (tx.type === 'credit') credits += tx.amount;
else debits += Math.abs(tx.amount);
}
return {
credits,
debits,
balance: credits - debits
};
});
</script>
<p>Credits: ${summary.credits} | Debits: ${summary.debits} | Balance: ${summary.balance}</p>
$effect: Side Effects
$effect runs code in response to reactive changes. It automatically tracks which reactive values it reads and re-runs when any of them change.
<script>
let query = $state('');
let results = $state([]);
$effect(() => {
if (query.length < 3) {
results = [];
return;
}
const controller = new AbortController();
fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal
})
.then(r => r.json())
.then(data => { results = data; })
.catch(() => {});
return () => controller.abort();
});
</script>
<input bind:value={query} placeholder="Search (3+ chars)...">
<ul>
{#each results as result}
<li>{result.title}</li>
{/each}
</ul>
When to Use $effect
Use $effect for genuine side effects:
- Fetching data from APIs.
- Setting up event listeners or subscriptions.
- Updating
document.titleor other browser APIs. - Synchronizing with external systems (localStorage, WebSocket).
- Logging or analytics.
Do not use $effect to compute derived values. If you find yourself writing $effect(() => { someVar = computation(); }), that should be $derived instead.
Cleanup
The function returned from $effect runs before the next re-execution and when the component is destroyed:
<script>
let theme = $state('light');
$effect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e) => { theme = e.matches ? 'dark' : 'light'; };
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
});
</script>
The Reactivity Contract
Svelte's reactivity follows predictable rules:
- $state changes are synchronous, but DOM updates are batched. If you update three state variables, Svelte batches the DOM updates into a single microtask.
- $derived values are lazy. They only recalculate when read, not immediately when dependencies change.
- $effect runs after DOM updates. When an effect runs, the DOM already reflects the latest state. This makes it safe to measure DOM elements inside effects.
- Dependencies are tracked automatically. You never declare a dependency list. Svelte observes which reactive values are actually read during execution.
- Reactivity is per-assignment for primitives.
count++triggers an update. For objects/arrays with$state, mutations are tracked through the proxy.
<script>
let a = $state(1);
let b = $state(2);
let sum = $derived(a + b);
$effect(() => {
// This effect depends on `sum`, which depends on `a` and `b`.
// Changing either `a` or `b` triggers `sum` to recalculate,
// which triggers this effect.
console.log(`Sum is now: ${sum}`);
});
</script>
Common Pitfalls
- Using derived: This creates unnecessary indirection and can cause extra render cycles. If the value is a pure function of other state, use
$derived. - **Destructuring state({ x: 1, y: 2 })
gives you non-reactive copies. Keep the object reference:let point = $state({ x: 1, y: 2 })and accesspoint.x`. - Reading reactive values conditionally in $effect: If your effect only reads a value inside an
ifbranch, the dependency is only tracked when that branch executes. This can lead to effects not re-running when you expect. - **Heavy computation in derived
recalculates when dependencies change, so an expensive filter or sort on a large array runs on every dependency change. Consider$state.raw` for the source data if appropriate. - Forgetting cleanup in $effect: Subscriptions, intervals, and event listeners created in an effect must be cleaned up via the return function to avoid memory leaks.
Key Takeaways
$stateis for mutable values that change over time and trigger UI updates. Objects and arrays are deeply reactive by default.$derivedis for computed values that stay in sync with their dependencies automatically. Use it instead of manually syncing state.$effectis for side effects: API calls, event listeners, browser API access. It runs after DOM updates and supports cleanup.$state.rawopts out of deep reactivity for large datasets or values you replace rather than mutate.- Dependencies are tracked automatically. There are no dependency arrays to manage.
- The golden rule: if a value can be computed from other state, it should be
$derived, not$statesynced via$effect.