Legitimate Use Cases
We’ve said it before: most components don’t need $effect. But “most” isn’t “all.” Here are the legitimate cases where $effect is the right tool.
The test: “Am I reaching outside the component to interact with the world?” If yes, you probably need an effect.
DOM Manipulation
When you need to work with the actual DOM, not Svelte’s abstraction:
<script>
let canvas;
let width = $state(400);
let height = $state(300);
let color = $state('#3498db');
$effect(() => {
const ctx = canvas.getContext('2d');
ctx.fillStyle = color;
ctx.fillRect(0, 0, width, height);
});
</script>
<canvas bind:this={canvas} {width} {height}></canvas>
<input type="color" bind:value={color} /> The effect runs whenever color, width, or height changes. Canvas isn’t reactive—you have to imperatively draw.
Third-Party Libraries
Libraries that manage their own DOM need initialization:
<script>
import Chart from 'chart.js/auto';
let canvas;
let data = $state([12, 19, 3, 5, 2, 3]);
$effect(() => {
const chart = new Chart(canvas, {
type: 'bar',
data: {
labels: data.map((_, i) => `Item ${i + 1}`),
datasets: [{ data }]
}
});
return () => chart.destroy();
});
</script>
<canvas bind:this={canvas}></canvas> The cleanup function (chart.destroy()) runs before the effect re-runs and when the component unmounts.
Event Listeners
For events that aren’t on elements you render:
<script>
let windowWidth = $state(0);
let scrollY = $state(0);
$effect(() => {
function handleResize() {
windowWidth = window.innerWidth;
}
function handleScroll() {
scrollY = window.scrollY;
}
// Set initial values
handleResize();
handleScroll();
window.addEventListener('resize', handleResize);
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('resize', handleResize);
window.removeEventListener('scroll', handleScroll);
};
});
</script>
<div class="scroll-indicator" style="width: {(scrollY / document.body.scrollHeight) * 100}%">
</div> WebSocket/Real-time Connections
<script>
let messages = $state([]);
let connected = $state(false);
let roomId = $state('general');
$effect(() => {
const ws = new WebSocket(`wss://chat.example.com/${roomId}`);
ws.onopen = () => {
connected = true;
messages = []; // Clear messages when switching rooms
};
ws.onmessage = (event) => {
messages.push(JSON.parse(event.data));
};
ws.onclose = () => {
connected = false;
};
return () => ws.close();
});
</script> When roomId changes, the old WebSocket closes and a new one opens to the new room.
Document/Browser APIs
<script>
let title = $state('My App');
let favicon = $state('/favicon.ico');
// Update document title
$effect(() => {
document.title = title;
});
// Update favicon
$effect(() => {
const link = document.querySelector("link[rel~='icon']");
if (link) {
link.href = favicon;
}
});
</script> Keep effects focused. One effect per concern.
Timers and Intervals
<script>
let running = $state(false);
let seconds = $state(0);
$effect(() => {
if (!running) return;
const id = setInterval(() => {
seconds++;
}, 1000);
return () => clearInterval(id);
});
</script>
<p>{seconds} seconds</p>
<button onclick={() => running = !running}>
{running ? 'Stop' : 'Start'}
</button>
<button onclick={() => seconds = 0}>Reset</button> The effect only sets up the interval when running is true.
Local Storage Sync
<script>
let theme = $state('light');
// Load from localStorage on mount
$effect(() => {
const saved = localStorage.getItem('theme');
if (saved) {
theme = saved;
}
});
// Save to localStorage on change
$effect(() => {
localStorage.setItem('theme', theme);
});
</script> Or combined:
<script>
let theme = $state(
typeof localStorage !== 'undefined'
? localStorage.getItem('theme') || 'light'
: 'light'
);
$effect(() => {
localStorage.setItem('theme', theme);
});
</script> Focus Management
<script>
let input;
let shouldFocus = $state(false);
$effect(() => {
if (shouldFocus && input) {
input.focus();
}
});
</script>
<input bind:this={input} />
<button onclick={() => shouldFocus = true}>Focus Input</button> The Checklist
Before writing $effect, ask:
- Can I use
$derived? If you’re computing a value, probably yes. - Can I use an event handler? If this happens in response to user action, probably yes.
- Can I use a load function? If you’re fetching data, probably yes.
- Am I interacting with external APIs, the DOM, or the browser? If yes,
$effectis likely correct.
Effect Best Practices
Keep effects small. One effect, one concern.
<script>
// Good: separate effects for separate concerns
$effect(() => {
document.title = pageTitle;
});
$effect(() => {
analytics.trackPage(currentPage);
});
</script> Always clean up. If you create something, destroy it.
<script>
$effect(() => {
const subscription = store.subscribe(handler);
return () => subscription.unsubscribe();
});
</script> Don’t set state that triggers the same effect. This creates infinite loops.
<script>
// BAD: infinite loop
let count = $state(0);
$effect(() => {
count = count + 1; // Effect runs, sets count, effect runs again...
});
</script> Effects are powerful. Use them only where they’re the right tool—and when they are, they work beautifully.