Svelte $effect Overuse: The React Reflex A bug shipped in a Svelte CMS feature, caused by overusing the $effect primitive instead of $derived, highlights a widespread reflex imported from React's useEffect pattern, according to the article's author. The bug passed 1,300+ tests and type checking but failed due to ordering races, and the author notes that AI coding agents, trained on React-heavy code, often replicate this pattern. The article warns that state-syncing effects incur extra update passes, no laziness, loop risks, and SSR blind spots, citing an audit of 72 call sites. The $effect Trap: Why Svelte's Escape Hatch Gets Overused This week, while adding a small feature to our CMS, we shipped a bug that never should have existed. The code passed type checking, passed 1,300+ tests, and worked in the demo. It was also built on a Svelte primitive that the framework's own documentation https://svelte.dev/docs/svelte/$effect politely describes as "something of an escape hatch," and it failed in exactly the way the documentation warns about https://svelte.dev/docs/svelte/$effect When-not-to-use-$effect . The primitive is $effect . The interesting part isn't the bug. It's why the code got written that way in the first place: the same reflex that produced it is shaping code written by thousands of developers and, increasingly, by AI coding agents. Ours included. A reflex imported from React If you learned modern frontend development in the last decade, there's a good chance you learned React first. And React taught a generation of developers one big idea about reactivity: when something changes, run a function . useEffect became the hammer for everything: deriving state, syncing state, fetching data, subscribing, resetting forms. The React team has spent years walking that back "You Might Not Need an Effect" https://react.dev/learn/you-might-not-need-an-effect is practically required reading now , but the muscle memory is deep. Svelte 5's $effect looks like useEffect . It's named like it. It even re-runs on dependency changes like it. So the reflex transfers: state changed, so I'll write an effect to update this other state. But Svelte is built on a different model. Reactivity is declarative and pull-based : $derived values recompute lazily, only when read, only when their dependencies actually changed. The framework wants you to describe what a value is, not when to update it. $effect exists for the boundary where declarative reactivity ends: drawing on a canvas, talking to a third-party library, managing a timer. Using it to synchronize one piece of state with another means fighting the framework with the framework's own tools. There's a second population with this exact reflex: AI coding agents. Training corpora are saturated with a decade of React, including every useEffect -to-sync-state pattern ever committed to GitHub. Ask an agent to "seed this form from server data" in a Svelte project and there's a decent chance you get an effect, because in the training distribution, that's what seeding state looks like. The author of the effect that started this post was an AI agent. The reviewer who flagged it was a human who read the docs. What it costs you State-syncing effects aren't just stylistically wrong. They have a real runtime cost profile: - Extra update passes. Effects run after rendering. An effect that writes state schedules another round of invalidation and re-rendering after the one that just finished. Chain a few together and one user interaction becomes a waterfall of update cycles, each waiting for the previous flush. - No laziness, no memoization. A $derived recomputes only when read, and short-circuits downstream updates when the value is referentially unchanged. An effect runs every time its dependencies tick, whether or not anyone needed the result. - Loops and races. An effect that reads and writes overlapping state is one refactor away from an infinite loop; Svelte will throw at you if you're lucky. Less lucky: the effect update depth exceeded ordering races. Our bug was one of these. An effect seeded local form drafts from server data, and a save handler cleared the drafts before refreshing the data, so the effect re-seeded from stale values. No error, no crash. Just quietly wrong data in the form. - SSR blind spots. Effects don't run during server-side rendering. State computed in an effect simply doesn't exist in the server-rendered output. The audit: 72 call sites After fixing our bug the idiomatic version: component-local state initialized from props, remounted via { key} when the server value changes, no effect at all , we did what you should do after any "how did this happen" moment: we went looking for the rest. Our CMS had 72 real $effect call sites across 46 files. The verdict after a full review: - Two were state-sync effects that converted directly to $derived or an attachment. One became the bug fix above. - The rest were legitimate: contenteditable DOM seeding, popover positioning, autosave timers, canvas rendering, localStorage persistence. Genuine side effects with no declarative home. That ratio is worth sitting with. $effect isn't evil, and a codebase full of them isn't necessarily wrong; imperative boundaries are real. The problem is unexamined effects: nobody had ever asked, of each one, "is this actually a side effect, or is it derived state wearing a trench coat?" The audit also shook loose an unrelated copy-pasted bug in ten files: a === where an = belonged, silently defeating a state reset for years. Audits pay for themselves. Svelte gives you a ladder of better tools, and almost every rung got used in our sweep: for anything computable from other state. Writable since Svelte 5.25, which covers the "derived but user-overridable" case too. $derived Function bindings https://svelte.dev/docs/svelte/bind Function-bindings bind:value={ = get, set} for linked inputs. Svelte 5.29+ for element-scoped DOM setup. The attachment receives the element, re-runs when its reactive inputs change, and cleans up via its return function. Our QR-code component went from {@attach} bind:this plus an effect to a five-line attachment.and