$effect
The \$effect rune runs side effects after the DOM updates — subscriptions, manual DOM work, syncing to external stores. Cleanups run on dependency change and on component teardown.
Effect, cleanup, dependency tracking
EXAMPLE
<script>
let count = $state(0);
let url = $state('/api/posts');
let online = $state(navigator.onLine);
// 1) Basic — runs on mount and any time count changes
$effect(() => {
document.title = `Count: ${count}`;
});
// 2) Cleanup — return a function
$effect(() => {
const id = setInterval(() => count++, 1000);
return () => clearInterval(id);
});
// 3) Listen to window events
$effect(() => {
const on = () => online = navigator.onLine;
window.addEventListener('online', on);
window.addEventListener('offline', on);
return () => {
window.removeEventListener('online', on);
window.removeEventListener('offline', on);
};
});
// 4) Async work — race-safe with an `active` flag
let posts = $state([]);
$effect(() => {
let active = true;
(async () => {
const res = await fetch(url);
const data = await res.json();
if (active) posts = data;
})();
return () => { active = false; };
});
// 5) $effect.pre — runs BEFORE DOM updates (instead of after)
$effect.pre(() => {
if (window.scrollY > 100) {
// measure something before the new render commits
}
});
// 6) Tracked vs untracked — only reads inside the effect become dependencies
let a = $state(0), b = $state(0);
$effect(() => {
console.log('runs when a changes:', a);
// reading b synchronously here would make it a dependency too
});
// To explicitly NOT track a read, use untrack from svelte
import { untrack } from 'svelte';
$effect(() => {
console.log(`a=${a}, b=${untrack(() => b)}`);
// runs only when a changes; b changes silently
});
// 7) $effect.root — start an effect outside a component
// Useful for global subscriptions in .svelte.js modules
// const stop = $effect.root(() => {
// $effect(() => { ... });
// return () => { /* cleanup */ };
// });
// stop(); // tear down everything when done
// 8) Don't mutate state INSIDE an effect — leads to infinite loops
// BAD:
// $effect(() => { count = count + 1; }); // infinite
// 9) Don't use $effect for derived values — use $derived
// BAD: let double = $state(0); $effect(() => { double = count * 2; });
// GOOD: let double = $derived(count * 2);
// 10) Common patterns
// a) Sync to localStorage
let theme = $state(localStorage.getItem('theme') ?? 'light');
$effect(() => {
localStorage.setItem('theme', theme);
document.documentElement.setAttribute('data-theme', theme);
});
// b) Subscribe to an external store
$effect(() => {
const unsub = userStore.subscribe(u => /* … */ {});
return unsub;
});
// c) Auto-focus when a condition becomes true
let inputEl;
let isEditing = $state(false);
$effect(() => {
if (isEditing) inputEl?.focus();
});
</script>
<input bind:this={inputEl} />
<button onclick={() => isEditing = !isEditing}>Toggle</button>
<p>Online: {online}</p>
Why it matters
\$effect is for syncing to the world (DOM, network, storage). For derived values, use \$derived — effects can’t replace it, and writing state inside an effect is the most common new-Svelte mistake.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
$effect(() => {
console.log('count is', count);
return () => { /* cleanup */ };
});
Try it Yourself »
Exercise
Run a side effect when deps change.
(() => console.log(count));
Starts with $.
Discussion
Loading…