Svelte 5 runes vs legacy let-based reactivity
- Last updated
- Prerequisites:
- Svelte 4 component basics
- Understanding of the Svelte compiler
- svelte
- runes
- reactivity
- signals
- svelte5
- compiler
Read at your depth
The practical view
In Svelte 4, reactivity was implicit and compiler-driven: let count = 0; created a reactive variable, and count += 1 updated the DOM. But that magic only worked inside .svelte component top-level scope. Svelte 5 introduces runes — explicit markers: let count = $state(0); creates reactive state anywhere (even in .js/.ts modules); $derived(expr) replaces the $: label; $effect(() => ...) replaces the reactive statement for side effects; and $props() replaces export let. Code written with runes works in plain modules, so state logic is extractable and testable outside components.
The same idea in other frameworks
const [count, setCount] = useState(0); // setCount triggers re-renderconst count = signal(0); // Angular signals, same runes-like primitiveconst count = ref(0); // ref.value is the tracked boxlet count = $state(0); // rune: compiler rewrites into a signal sourceLegacy vs modern
Reactive state in a shared module: Svelte 4 vs runes
Svelte 4 reactivity only worked inside components; runes make state a first-class primitive usable in plain modules, extractable and testable.
<script>
let count = 0; // implicit reactivity: compiler-scoped
function inc() { count += 1; }
</script>export let count = $state(0);
export function increment() { count += 1; }Interview gotchas
Context
Interviewers want to see the 'why' behind the API change: the failure modes of static analysis, not just the new syntax.
The mechanical answer
Svelte 4's reactivity was a compile-time heuristic: the compiler tracked assignments inside the component to mark invalidations, which broke when state was mutated indirectly — through destructuring, through nested object/array writes in some helper patterns, or when shared across modules. State also could not be extracted to .js modules, forcing components to stay fat. Runes make reactivity explicit and value-based: the runtime tracks reads/writes of the wrapped value, so indirect mutation and cross-module sharing work correctly. The explicit marker also lets the compiler optimize without guessing what a bare variable means.
Trap
The naive answer is 'runes are faster'. The speed story is nuanced — both versions compile to direct DOM updates. The real motivation is correctness and expressiveness: cross-module state, indirect mutation, and predictable semantics. Candidates who answer purely with 'it's the modern way' miss that Svelte 4's model silently failed in real codebases, which is the actual driving force.
Context
This probes signal-graph literacy — the interviewer wants to know if the candidate understands derived values as lazily recomputed state rather than effects.
The mechanical answer
$state declares mutable reactive state. $derived declares a lazy computed value that recomputes only when its dependencies change and only when something reads it — it is the right tool for values derived from state. $effect runs a function whenever the reactive values it reads change; it is meant for side effects: DOM interop, timers, subscriptions, logging. Using $effect to compute a value is an anti-pattern because effects are eager, imperative, and re-run on every dependency change, producing redundant work and making the data flow harder to trace. The compiler enforces that you cannot assign to a $derived, mirroring the immutability contract of computed values.
Trap
The trap answer is '$derived is a memoized effect'. It is not an effect at all — it is a lazy derived source in the signal graph, evaluated on demand, whereas effects are eager and triggered by the scheduler. Another trap: using $effect to synchronize state (effect reads a, writes b) — the runes model intentionally discourages 'state derived from state' in effects because it causes cascade re-runs; derive it instead. Mentioning the microtask-batched scheduler of effects is a strong differentiator.