Effects, Lifecycle, and Fetching
Everything so far - state, derivations, templates - lives inside Svelte's world, where the
compiler wires changes to updates. But apps have to talk to the world outside: timers, network,
localStorage, chart libraries, the document title. $effect is that bridge, and it comes with
the same warning label as its cousins in React and Vue: the moment you use an effect to manage
your own state instead of the outside world, you've built a bug. This phase draws the line
precisely.
$effect: the shape
What just happened: the effect ran after the component mounted, and re-runs whenever query
changes. Which state it depends on is discovered the Svelte way - by what the function actually
reads - no dependency array to maintain, same auto-tracking as $derived, applied to actions
instead of values.
Cleanup is the return value:
The returned function runs before the effect re-runs, and when the component unmounts - one mechanism, both moments. Whatever an effect starts, its cleanup stops: intervals, listeners, subscriptions, observers. An effect with a start and no stop is a leak with a delay on it.
The rule: effects face outward
The most common misuse, in every framework, is the same:
Svelte is unusually opinionated here: writing to state that the same effect also (transitively)
depends on raises state_unsafe_mutation errors or effect-loop warnings, and even when it runs,
you've created a second source of truth that updates a beat late. The line to internalize:
💡 Key point: $derived answers "what is this value?" - $effect answers "what should
happen when this changes?" If the sentence ends in a value, derive. If it ends in an action on
the outside world (the DOM directly, a timer, the network, storage, a library), effect. Svelte's
docs say it plainly: if you're synchronizing state inside an effect, you almost always want
$derived instead.
onMount and DOM-dependent setup
$effect doesn't run during server-side rendering (there's no browser to affect), and it first
runs after the component is in the DOM - so for most "when the component appears" work, $effect
is the mount hook. The older onMount import still exists and still matters in one common case:
it can be async (an effect's function can't be, since its return value must be the cleanup):
{#if error}Couldn't load orders.
{:else if !orders}Loading…
{:else}{#each orders as o (o.id)}{o.ref}{/each}{/if}
(Or skip the flags entirely: assign the promise to state and let phase 3's {#await} render the
three states. Both are idiomatic; {#await} is less code, explicit flags give you more control
over layout.)
Refetch-on-change, with the race handled
Fetching again when a value changes is effect territory - an outside-world action triggered by state. The out-of-order response race comes along, and cleanup is the fix:
What just happened: switching selectedId from 1 to 2 runs the cleanup (cancelling 1's
in-flight interest) before re-running the effect for 2 - so if 1's response arrives late, it finds
cancelled true and touches nothing. Same pattern as React's effect flag and Vue's onCleanup,
wearing Svelte's return-function syntax.
⚠️ Gotcha: auto-tracking only registers what the effect reads synchronously. Reads inside
.then callbacks, await continuations, or setTimeout happen after tracking closed - they
don't subscribe the effect to anything. The const id = selectedId line isn't decoration: it's
the synchronous read that puts selectedId on the effect's dependency list. Read your triggers at
the top, then go async.
Recap
$effect= auto-tracked side effects; dependencies are what it reads synchronously; the returned function is cleanup (pre-re-run and unmount).- Effects face outward - deriving state in an effect earns you
state_unsafe_mutationand a stale copy. Values are$derived's job. $effectalready covers "on mount" for browser work;onMountremains the async-friendly spelling for load-once fetches ({#await} being the low-ceremony alternative).- Refetch effects: read triggers at the top, cancel via cleanup, ignore stale responses.
- Every start needs a stop - intervals and listeners without cleanup outlive their component.
[
{
"q": "An effect fetches a product and reads selectedId only inside the .then callback. Changing selectedId doesn't refetch. Why?",
"choices": [
"Effects only run once unless given a dependency array",
"Dependencies are tracked from synchronous reads - a read inside .then happens after tracking has closed, so the effect never subscribed to selectedId",
"fetch calls can't be tracked by the compiler",
"The effect needs to be marked async"
],
"answer": 1,
"why": [
"Svelte effects have no dependency arrays - tracking is automatic, but only over synchronous reads.",
null,
"The fetch itself is irrelevant to tracking - what matters is when state is read.",
"Effect functions can't be async at all (the return value must be the cleanup) - and that wouldn't fix the tracking window."
],
"explain": "Auto-tracking records reads made while the effect body runs synchronously. Read your trigger at the top (const id = selectedId), then use it in async code freely."
},
{
"q": "let total = $state(0) kept in sync by an $effect that reads items and writes total. Svelte complains, and the docs agree. What's the right shape?",
"choices": [
"Move the write into a setTimeout so it runs outside tracking",
"let total = $derived(items.reduce(...)) - it's a value, not an action",
"Split it into two effects, one reading and one writing",
"Mark total as $bindable"
],
"answer": 1,
"why": [
"Deferring the write dodges the error and keeps the stale-copy design - worse, not better.",
null,
"However you slice the effects, state-syncing-state remains a second source of truth.",
"$bindable is a component-prop contract - unrelated to derivation."
],
"explain": "The question 'what is total?' has an answer expressible from existing state - that's a derivation. Effects are for actions on the world outside Svelte's state graph."
},
{
"q": "A component's effect starts a WebSocket connection. What must the effect also do?",
"choices": [
"Nothing - Svelte closes connections when components unmount",
"Return a cleanup function that closes the socket",
"Wrap the connection in onMount instead",
"Store the socket in $state so it's tracked"
],
"answer": 1,
"why": [
"Svelte tears down its own wiring - browser resources you open are yours to close.",
null,
"onMount changes when it starts, not who stops it - a cleanup is needed there too.",
"Tracking a socket object does nothing; sockets aren't state, they're resources."
],
"explain": "Effects that acquire resources return the release: the cleanup runs on re-run and unmount, so the socket's lifetime matches the component's."
}
]
← Phase 5: Sharing State · Guide overview · Phase 7: When Svelte Breaks →
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 3 questions
1. An effect fetches a product and reads selectedId only inside the .then callback. Changing selectedId doesn't refetch. Why?
2. let total = $state(0) kept in sync by an $effect that reads items and writes total. Svelte complains, and the docs agree. What's the right shape?
3. A component's effect starts a WebSocket connection. What must the effect also do?