All posts

Svelte 5 runes, coming from React

What actually changes when reactivity is a value instead of a hook — and the class of bugs that stops existing.

Published
  • Svelte
  • React
  • TypeScript
Contents

I have written a lot of React. Moving to Svelte 5 for a side project, the thing that surprised me was not what runes add — it is what they remove.

Dependency arrays stop existing

The React version of a derived value:

const total = useMemo(() => items.reduce((a, b) => a + b.price, 0), [items])

You have to tell React what this depends on. Get the array wrong and you ship a stale value; get it too broad and you recompute constantly. Neither failure is loud.

The Svelte version:

let total = $derived(items.reduce((a, b) => a + b.price, 0))

There is no array because the compiler already knows. It tracked the read of items when the expression ran. This is not a nicer API for the same idea — it is a different idea, where the dependency graph is discovered rather than declared.

Stale closures go with it

The bug I have spent the most cumulative hours on in React:

useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000)
  return () => clearInterval(id)
}, []) // count is captured at 0, forever

$state is not a snapshot captured at render, so the equivalent does not have the trap. There is no render to capture from.

What is genuinely harder

Two things got worse, not better:

$effect invites misuse. It looks like useEffect, so the reflex is to reach for it — but most of what you would put in useEffect belongs in $derived. The rule I settled on: if the effect ends by assigning to state, it should have been a derived value.

Reactivity is not deep by default in the way you expect. $state on an object gives you a proxy, which is mostly what you want, but reassigning versus mutating still produces different results and the difference is not always visible at the call site.

Where it lands

For a component with three pieces of state and a couple of derived values, the Svelte version is meaningfully shorter and has fewer places to be wrong. For a large app, I have not run the experiment. But the specific bugs I stopped writing were real ones, and I do not miss them.