We maintain a Svelte component and a React codebase in the same week, most weeks. Not because we’re indecisive about frontend frameworks, but because the two projects have different jobs. FurnitureAxis is React 19. This site is Svelte for its one interactive piece, the mascot described in why this site loads in under a second. Switching between them all week is a decent way to notice what each model actually asks of you.
Both frameworks solve the same three problems: hold a value, derive something from it, and run a side effect when it changes. Svelte 5’s runes and React’s hooks name these jobs almost identically: $state and useState, $derived and useMemo, $effect and useEffect. The names rhyme. The mechanics underneath don’t.
Where the reactivity actually lives
Svelte’s runes are keywords the Svelte compiler recognizes at build time. When you write let count = $state(0), the compiler rewrites reads and writes of count into getter and setter calls behind the scenes. You keep writing count++ and the transformed code handles notifying whatever depends on it. For objects and arrays, $state goes further and returns a deeply reactive proxy, so pushing to an array or setting a nested property is enough to trigger an update, according to Svelte’s own docs.
React hooks don’t get a compile step that changes their meaning. useState is a plain function call that returns a two-item array: the current value and a setter, per the React docs. Calling the setter doesn’t mutate anything in place. It queues a re-render, and on that re-render your component function runs again from the top, so count on the next pass is a fresh local variable that happens to hold the new value. That’s why React needs the rules of hooks: same order, every render, no hooks inside conditionals. React matches hooks to state by call order, so a hook skipped in one render throws the whole chain off.
Here’s the same counter increment in each, so the shapes sit next to each other. Svelte lets you touch count directly; React routes every update through the setter:
let count = $state(0);
count++;
const [count, setCount] = useState(0);
setCount(count + 1);
Derived values: tracked automatically vs recomputed on a list you write
$derived recalculates when anything it read synchronously changes, and Svelte figures out that dependency list for you by watching what the expression touches, per the derived state docs. You write let doubled = $derived(count * 2) and never list count anywhere. Add a second variable to the expression and Svelte tracks that one too, automatically.
useMemo caches a computed value between renders, but you supply the dependency array yourself: useMemo(() => count * 2, [count]). Per the React docs, if you leave a dependency out, the memoized value goes stale and doesn’t update when it should. Forget to add one, and you’ve shipped a bug that only shows up when a user hits the right sequence of state changes. This is the tradeoff in miniature: Svelte’s compiler does the bookkeeping, React hands you the bookkeeping and a lint rule to catch you when you get it wrong.
React does have an answer to this now. The React Compiler can insert memoization automatically, cutting down on how often you’d reach for useMemo by hand. It’s still a build tool you opt into, separate from the hooks themselves. Svelte’s compiler works differently: writing let x = $state(0) only means anything because the compiler is always there to transform it. React’s hooks run the same with or without the compiler installed; the compiler just makes some manual calls redundant.
Effects: after the DOM, either way
$effect and useEffect actually agree on timing. Both run after the browser has applied DOM updates, so neither blocks the paint. Svelte batches effect re-runs and figures out dependencies the same way $derived does, by watching what you read inside the function, according to the effect docs. React’s useEffect needs the dependency array again, and the same missing-dependency failure mode applies, this time as a stale closure inside the effect instead of a stale memoized value. Both support a cleanup function returned from inside, which runs before the effect re-runs and on unmount, per React’s docs.
What this means day to day
Svelte’s automatic tracking means less code to write and fewer dependency-array bugs to chase, because there’s no array to get wrong. The cost is that the mechanism is less visible. When something over-updates or under-updates, you’re debugging what the compiler decided counts as a dependency, not a list you wrote yourself.
React’s explicit arrays are more code and a sharper edge to cut yourself on, but the dependency list is a real, readable artifact. You can look at [todos, tab] and know exactly what triggers a recompute, without knowing anything about how the compiler transforms the file. For a codebase with a large data model and a team larger than one, that explicitness is worth something: a wrong array is a diff a reviewer can catch, where a wrong compiler inference is a mystery you reproduce in the browser.
Neither model is more correct. Svelte’s approach fits a small, tightly scoped piece of UI like a mascot that watches scroll position and does one thing. React’s explicit hooks fit a large, multi-person frontend where you want dependencies spelled out in the code rather than inferred by a compiler pass. That’s also roughly why the split on our own projects looks the way it does: one framework for the SaaS product with a big team surface, the other for a static site with a single animated island.
If you’re choosing between them for a new project, ask what you’re actually optimizing for: less code to maintain in a small surface, or explicit, reviewable dependencies in a large one. Tell us what you’re building and we’ll tell you which side of that line it falls on.