Skip to content

The $derived Rune

$derived declares a value computed from other reactive values. It recomputes automatically whenever any value it reads changes — and it is cached, so it only recomputes when a dependency actually changed.

<script>
let count = $state(2);
let double = $derived(count * 2); // 4, and stays in sync
let label = $derived(`count is ${count}`);
</script>
<p>{double} — {label}</p>

You never list dependencies. The compiler sees that double reads count and rebuilds double exactly when count changes. Derived values are read-only by default — you don’t normally assign to them; they are a function of their inputs. (There is one deliberate exception — see “Writable deriveds” below.)

Compile this exact example below and look for $.derived(() => count * 2) in the output — that’s the whole rune: a lazily-recomputed cell wrapping the function you wrote, not a re-run of the entire component.

<script>
// English comments: keep this pinned to Svelte 5 runes mode.
let count = $state(2);
let double = $derived(count * 2);
</script>
<button onclick={() => count++}>{double}</button>

$derived(expr) takes a single expression. When the computation needs multiple statements — a loop, a temporary variable, a branch — use $derived.by with a function that returns the value.

<script>
let numbers = $state([1, 2, 3, 4]);
let stats = $derived.by(() => {
let sum = 0;
for (const n of numbers) sum += n;
return { sum, avg: sum / numbers.length };
});
</script>
<p>sum {stats.sum}, avg {stats.avg}</p>

$derived(x) is just shorthand for $derived.by(() => x). Dependencies are still tracked automatically — anything the function reads becomes a dependency.

flowchart LR
  a["$state count"] --> d["$derived double = count * 2"]
  b["$state numbers"] --> e["$derived.by stats"]
  d --> ui["DOM updates when inputs change"]
  e --> ui
Derived values recompute from their tracked dependencies

$derived is often described as “automatically recomputing,” which makes it sound like it re-runs the moment a dependency changes. It doesn’t. Svelte’s reactivity is push-pull: a dependency change pushes a “you’re dirty” notification to the derived, but the derived’s function only actually re-runs — is pulled — the next time something reads it. If nothing ever reads it, it never re-runs at all, no matter how many times its dependencies change.

src/lib/derived-laziness.svelte.test.ts
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
test('$derived is push-pull: dependency changes mark it dirty, but it only re-evaluates on next READ', () => {
let evaluations = 0;
const destroy = $effect.root(() => {
let count = $state(0);
let double = $derived.by(() => {
evaluations++;
return count * 2;
});
flushSync();
expect(evaluations).toBe(0); // never read yet — nothing has evaluated it
void double; // read #1
expect(evaluations).toBe(1);
void double; // read #2, no dependency changed — cached, no re-evaluation
expect(evaluations).toBe(1);
// Change the dependency THREE times without reading `double` in between.
count = 1;
count = 2;
count = 3;
flushSync();
expect(evaluations).toBe(1); // still 1 — "push" only marked it dirty, didn't recompute
void double; // one read now triggers exactly one re-evaluation, for the LATEST value
expect(evaluations).toBe(2);
expect(double).toBe(6); // 3 * 2, not 2 or 4 — it skipped the intermediate values entirely
});
destroy();
});
✓ $derived is push-pull: dependency changes mark it dirty, but it only re-evaluates on next READ
Test Files 1 passed (1)
Tests 1 passed (1)

The practical consequence: a $derived that is never read (dead code, or a value only used behind a condition that’s never true) costs nothing at runtime beyond the initial declaration — proven by changing its dependency five times and observing zero evaluations:

✓ an unread $derived never evaluates at all, even across many dependency changes
Test Files 1 passed (1)
Tests 1 passed (1)

This is also why a $derived must stay free of side effects: you cannot rely on it running a specific number of times, or at a specific moment — only that its value will be correct the next time something reads it.

Before Svelte 5.25, deriveds were strictly read-only. Since 5.25, you can reassign a $derived value — the override holds until the next time a real dependency change forces a recompute, at which point it snaps back to being derived. The canonical use case is optimistic UI: show the new value immediately, then reconcile with the server.

<script>
let { post, like } = $props();
let likes = $derived(post.likes);
async function onclick() {
likes += 1; // optimistic: show it immediately
try {
await like(); // tell the server
} catch {
likes -= 1; // failed — roll back
}
}
</script>
<button {onclick}>🧡 {likes}</button>

Proven — the override is visible immediately, and a real dependency change discards it:

src/lib/derived-writable.svelte.test.ts
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
test('a $derived value can be reassigned (writable derived, since Svelte 5.25) for optimistic UI', () => {
const destroy = $effect.root(() => {
let serverLikes = $state(10);
let likes = $derived(serverLikes);
expect(likes).toBe(10);
// Optimistic bump: reassign the derived value directly, ahead of the server.
likes += 1;
expect(likes).toBe(11); // the override is visible immediately
// The override holds ONLY until a real dependency change forces a recompute.
serverLikes = 20; // server eventually reports the real count
flushSync();
expect(likes).toBe(20); // the manual override is gone — back to deriving from source
});
destroy();
});
✓ a $derived value can be reassigned (writable derived, since Svelte 5.25) for optimistic UI
Test Files 1 passed (1)
Tests 1 passed (1)

A const $derived cannot be reassigned — that’s a real compile error, not just a lint warning:

test('a $derived declared with `const` cannot be reassigned (compile error)', async () => {
const { compile } = await import('svelte/compiler');
const source = `
<script>
let count = $state(0);
const double = $derived(count * 2);
function bump() { double += 1; }
</script>
<button onclick={bump}>{double}</button>
`;
expect(() => compile(source, { generate: 'client' })).toThrowError();
});
✓ a $derived declared with `const` cannot be reassigned (compile error)
Test Files 1 passed (1)
Tests 1 passed (1)

Just like $state, $derived works as a class field, computed from other fields on the same instance:

<script>
class Cart {
items = $state([]);
total = $derived(this.items.reduce((sum, i) => sum + i.price * i.qty, 0));
add(price, qty) {
this.items.push({ price, qty });
}
}
const cart = new Cart();
</script>
<p>Total: {cart.total}</p>

Proven, recomputing correctly as items are added:

src/lib/derived-in-class.svelte.test.ts
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
class Cart {
items = $state<{ price: number; qty: number }[]>([]);
total = $derived(this.items.reduce((sum, i) => sum + i.price * i.qty, 0));
add(price: number, qty: number) {
this.items.push({ price, qty });
}
}
test('$derived works as a class field, recomputing from other reactive fields on the same instance', () => {
const log: number[] = [];
const destroy = $effect.root(() => {
const cart = new Cart();
$effect(() => {
log.push(cart.total);
});
flushSync();
cart.add(10, 2);
flushSync();
cart.add(5, 1);
flushSync();
});
destroy();
expect(log).toEqual([0, 20, 25]);
});
✓ $derived works as a class field, recomputing from other reactive fields on the same instance
Test Files 1 passed (1)
Tests 1 passed (1)

A common React-brain mistake is to compute a value in an effect and write it into state:

<script>
let count = $state(2);
let double = $state(0);
// ❌ Don't do this — an effect that mirrors derived state.
$effect(() => { double = count * 2; });
</script>

This is worse in every way: an extra state variable, an extra update cycle, a chance for double to be briefly out of sync, and harder-to-follow data flow. The Effect Rune lesson proves the “briefly out of sync” part with a running test — the effect-mirrored version genuinely lags by a beat. The right tool is $derived:

<script>
let count = $state(2);
let double = $derived(count * 2); // ✅ one source of truth, always in sync
</script>

Rule of thumb: if a value can be computed from other state, derive it — don’t store it and sync it. Reach for $effect only for genuine side effects (the next lesson), not for computing values.

Think of $derived as a cached formula, not a subscription. Declaring it doesn’t run anything; a dependency changing doesn’t run anything either — it only flips an internal “stale” flag. The formula runs exactly once, lazily, the next time someone asks for the value, and the answer is cached until the flag flips again. This is why an unread derived is free, why it must stay pure (you can’t count on when or how many times it runs), and why “overriding” it (writable deriveds) is really just replacing the cached answer until the next real recompute clears it.

ChoiceBenefitCost
$derived over $state + $effectOne source of truth, always consistent, no extra render cycleNone when the value is a pure function of other reactive state
$derived.by over $derivedHandles multi-statement logic (loops, branches)Slightly more ceremony than a single expression
Writable derived (optimistic override)Immediate UI feedback before a server round-tripOnly holds until the next real dependency change — don’t rely on it as durable storage
A $derived with a side effect—None — side effects inside $derived are unreliable by design: laziness means you cannot predict how many times, or when, they’ll fire
  • “$derived recomputes the instant its dependency changes.” It doesn’t — proven above. A dependency change only marks it dirty; the actual recompute happens lazily, on the next read.
  • “An unread $derived still costs something because its dependency changed.” Proven false: zero evaluations occurred across five dependency changes when nothing ever read the value.
  • “Putting a console.log or an API call inside $derived is fine, it’s basically the same as an effect.” It is not the same — a derived can run zero times, or be recomputed at a moment you don’t control, purely based on read timing. Side effects belong in $effect.
  • You may see state_referenced_locally from the compiler (“This reference only captures the initial value…”) when reading a $state/$derived variable directly in test-style code after reassigning it in the same scope. It’s a real, useful warning for closures held for later use (like setContext), but a plain synchronous read-and-assert in the same tick — as in the tests on this page — still observes the live, correct value.

When you see a $derived, check for these before trusting it:

  • Does the derived expression mutate state or call something with a side effect (an API call, console.log used for anything beyond debugging, pushing to an array declared outside it)? Svelte disallows state mutation inside $derived, but non-state side effects will compile and then misbehave.
  • Is a value computed in an $effect and written into a separate $state variable, when it could just be $derived directly from its inputs?
  • Is a $derived value reassigned somewhere expecting the override to be permanent, rather than understanding it lasts only until the next real recompute?
  • Is $derived.by used just to wrap a single expression (no benefit over plain $derived), or is plain $derived misused for logic that actually needs multiple statements?
  • Does a class mix reactive $derived fields with plain, non-reactive computed getters, inconsistently?

🐛 Spot the bug

function useAnalytics(events) {
let count = $state(0);
// "Track every time the filtered list changes" — logs a side effect
// inside the derived computation itself.
let filtered = $derived.by(() => {
const result = events.filter((e) => e.type === 'click');
trackAnalytics('filtered-recomputed', result.length); // side effect!
return result;
});
return { get filtered() { return filtered; } };
}
Show the answer

trackAnalytics(...) runs inside the $derived.by callback, which the author assumes fires exactly once per change to events — but $derived is push-pull and lazy: it only re-evaluates the next time filtered is actually read, and it never evaluates at all if nothing reads it. Proven in the probe: changing a dependency five times with no read in between produced zero evaluations of the derived function, and a single subsequent read produced exactly one evaluation reflecting only the latest state — the intermediate changes were silently skipped. So trackAnalytics here fires an unpredictable number of times — possibly never, possibly once for several batched changes — which is unusable for anything that needs to count real state transitions. The fix: move the tracking call into an $effect that reads filtered, since $effect is the rune that guarantees “run when this changes,” not $derived.

What does $derived do?
When do you use $derived.by instead of $derived?
What is evaluations at this point?
let evaluations = 0;
let count = $state(0);
let double = $derived.by(() => { evaluations++; return count * 2; });
count = 1;
count = 2;
count = 3;
flushSync();
// (nothing has read `double` yet)
On Svelte 5.25+, what happens when likes += 1 runs?
<script>
  let serverLikes = $state(10);
  let likes = $derived(serverLikes);
  likes += 1; // reassign the derived value
</script>
Why prefer $derived over an $effect that writes derived state?
A $derived.by computation calls an analytics-tracking function as a side effect, expecting it to fire once per change to its dependency. What is wrong with this?