Skip to content

$effect — Side Effects

useEffect has a well-known footgun: forget a variable in the dependency array and your effect runs with a stale value. Add too many and it runs too often. Svelte 5’s $effect solves this completely — there is no dependency array. The effect runs once, then re-runs automatically every time any reactive value it read during the previous run changes. The compiler tracks dependencies; you don’t.

React
function TitleSync() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
document.title = 'Count: ' + count;
}, [count]); // <-- must list count here
return <button onClick={() => setCount(c => c + 1)}>+1 (count: {count})</button>;
}
Svelte
<script>
let count = $state(0);
$effect(() => {
document.title = 'Count: ' + count;
// count is read here, so it is tracked automatically
});
</script>
<button onclick={() => count++}>+1 (count: {count})</button>

No [count]. The effect reads count, so Svelte knows to re-run it whenever count changes.

  • First run: after the component mounts (same as useEffect with no deps).
  • Re-runs: synchronously after any reactive dependency it read on the previous run changes.
  • Not during render: unlike useMemo, $effect never runs during the synchronous rendering phase.

Return a function from $effect to clean up before the next run or when the component is destroyed — identical to useEffect’s cleanup pattern.

React
function Timer() {
const [seconds, setSeconds] = React.useState(0);
React.useEffect(() => {
const id = setInterval(() => setSeconds(s => s + 1), 1000);
return () => clearInterval(id); // cleanup
}, []);
return <p>Elapsed: {seconds}s</p>;
}
Svelte
<script>
import { onMount } from 'svelte';
let seconds = $state(0);
onMount(() => {
const id = setInterval(() => seconds++, 1000);
return () => clearInterval(id); // cleanup
});
</script>
<p>Elapsed: {seconds}s</p>

For a “run once on mount + cleanup on destroy” pattern, onMount is cleaner than $effect. More on that in the Lifecycle lesson.

A common useEffect use case — syncing state to localStorage — maps directly:

React
function ThemeToggle() {
const [theme, setTheme] = React.useState(
() => localStorage.getItem('theme') ?? 'light'
);
React.useEffect(() => {
localStorage.setItem('theme', theme);
document.body.setAttribute('data-theme', theme);
}, [theme]);
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Theme: {theme}
</button>
);
}
Svelte
<script>
let theme = $state(
typeof localStorage !== 'undefined'
? (localStorage.getItem('theme') ?? 'light')
: 'light'
);
$effect(() => {
localStorage.setItem('theme', theme);
document.body.setAttribute('data-theme', theme);
});
</script>
<button onclick={() => theme = theme === 'light' ? 'dark' : 'light'}>
Theme: {theme}
</button>
How does $effect know which reactive values to re-run on?
How do you clean up a $effect (e.g. clear an interval)?
You want an effect to run exactly once after mount. What is the idiomatic Svelte approach?