Svelte and SvelteKit course · Module 3: Reactivity and Runes
$effect - Side Effects
In this lesson9
$effect is a rune that executes code in response to changes. Use it for synchronizing with the outside world.
Basic Usage
<script>
let count = $state(0);
// Executes on every change to count
$effect(() => {
console.log('Counter:', count);
});
</script>
<button onclick={() => count++}>
Increase ({count})
</button>When to Use $effect?
Good use cases:
- Logging/debugging
- Synchronization with localStorage
- Integration with external libraries
- Listening for events
- Timers and animations
Avoid:
- Updating other state (use
$derived) - Fetching data (use
loadin SvelteKit)
Example: localStorage
<script>
// Load the saved theme on startup (or use the default)
let theme = $state(localStorage.getItem('theme') ?? 'dark');
// Save to localStorage on every change
$effect(() => {
localStorage.setItem('theme', theme);
document.body.className = theme;
});
</script>
<select bind:value={theme}>
<option value="dark">Dark (Northern Night)</option>
<option value="light">Light (Day of the Sun)</option>
</select>We read the saved theme right in the initial value of $state, and the effect only saves changes. Do not split this into two effects - one saving and one loading - because the save runs first and overwrites the remembered theme. In SvelteKit a component also renders on the server, where there is no localStorage - reach for it only in the browser, e.g. in onMount (Valhalla, module 7).
Cleanup
$effect can return a cleanup function:
<script>
let isRunning = $state(false);
let seconds = $state(0);
$effect(() => {
if (!isRunning) return;
// Set interval
const interval = setInterval(() => {
seconds += 1;
}, 1000);
// Cleanup - clear on the next invocation
return () => {
clearInterval(interval);
};
});
</script>
<p>Time: {seconds}s</p>
<button onclick={() => isRunning = !isRunning}>
{isRunning ? 'Stop' : 'Start'}
</button>$effect.pre - Before DOM Update
<script>
let messages = $state([]);
// Executes BEFORE DOM update
$effect.pre(() => {
console.log('Before render, messages:', messages.length);
});
</script>Effect Dependencies
$effect automatically tracks dependencies:
<script>
let a = $state(1);
let b = $state(2);
let c = $state(3);
$effect(() => {
// This effect will run when a or b changes
// It will NOT run when c changes
console.log('a + b =', a + b);
});
</script>untrack - Reading Without Tracking
Sometimes an effect must read state but should not depend on it. A typical example is a change log: the effect reacts to count and appends an entry to the logs array. If it read logs the normal way, it would keep re-running itself, and Svelte would stop it with the effect_update_depth_exceeded error. The untrack function reads state without tracking it:
<script>
import { untrack } from 'svelte';
let count = $state(0);
let logs = $state([]);
$effect(() => {
const entry = `Count: ${count}`; // dependency: count
untrack(() => {
logs = [...logs, entry].slice(-5); // logs read without tracking
});
});
</script>
<button onclick={() => count++}>+1</button>
<ul>
{#each logs as log}
<li>{log}</li>
{/each}
</ul>Remember the rule though: if a value can be computed from others, use $derived, not an effect.
$inspect - Watching in the Console
For debugging, instead of an effect with console.log you can use the $inspect rune. It prints the value in the console on startup and after every change, including changes deep inside an object. It works only in development mode - it disappears from the built application:
<script>
let warrior = $state({ name: 'Ragnar', health: 100 });
$inspect(warrior); // logs the whole object after every change
</script>
<button onclick={() => warrior.health -= 10}>
{warrior.name}: {warrior.health} HP
</button>Example: Integration with an External Library
<script>
import Chart from 'chart.js/auto';
let loot = $state([10, 20, 30, 40]);
let canvas;
$effect(() => {
// The effect reads loot, so it runs again after every change
const chart = new Chart(canvas, {
type: 'bar',
data: {
labels: ['Ragnar', 'Bjorn', 'Ivar', 'Ubbe'],
datasets: [{ label: 'Loot', data: $state.snapshot(loot) }]
}
});
// Cleanup - destroy the old chart before a new one is created
return () => chart.destroy();
});
</script>
<canvas bind:this={canvas}></canvas>
<button onclick={() => loot[0] += 10}>Ragnar takes loot</button>The effect starts after the component is mounted, so canvas is already there. $state.snapshot(loot) gives the library a plain copy of the array instead of a reactive proxy - external libraries prefer plain data. Every change of the loot destroys the old chart in the cleanup function and draws a new one.
Code for this lesson: App.svelte
<script>
// $effect - side effects
import { untrack } from 'svelte';
let theme = $state('dark');
let count = $state(0);
let logs = $state([]);
// Logging effect: depends on count only.
// logs is read inside untrack - otherwise the effect would
// depend on logs, change it and re-run itself forever.
$effect(() => {
const entry = `Count: ${count}`;
untrack(() => {
logs = [...logs, entry].slice(-5);
});
});
// Effect with cleanup
let seconds = $state(0);
let isRunning = $state(false);
$effect(() => {
if (!isRunning) return;
const interval = setInterval(() => {
seconds += 1;
}, 1000);
// Cleanup - return a function
return () => clearInterval(interval);
});
// Effect on document
$effect(() => {
document.body.className = theme;
});
</script>
<main class="effects-demo">
<h1>$effect - Side effects</h1>
<section>
<h2>Logging effect</h2>
<p>Count: {count}</p>
<button onclick={() => count++}>+1</button>
<div class="logs">
<h3>Logs (last 5):</h3>
{#each logs as log}
<p>{log}</p>
{/each}
</div>
</section>
<section>
<h2>Effect with cleanup (Timer)</h2>
<p>Seconds: <strong>{seconds}</strong></p>
<button onclick={() => isRunning = !isRunning}>
{isRunning ? '⏹Stop' : '▶Start'}
</button>
<button onclick={() => seconds = 0}>Reset</button>
<p class="hint">Cleanup stops the interval when isRunning = false</p>
</section>
<section>
<h2>Effect on document</h2>
<select bind:value={theme}>
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
<p class="hint">Changes body class</p>
</section>
</main>
<style>
.effects-demo {
font-family: 'Georgia', serif;
max-width: 600px;
margin: 0 auto;
padding: 2rem;
background: #1a0f0a;
color: #e8d5b7;
}
h1, h2 {
color: #d4af37;
}
section {
background: rgba(212, 175, 55, 0.1);
border: 1px solid #d4af37;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
button {
padding: 0.5rem 1rem;
background: #3d2914;
color: #e8d5b7;
border: 1px solid #d4af37;
border-radius: 4px;
cursor: pointer;
margin-right: 0.5rem;
}
select {
padding: 0.5rem;
background: #2c1810;
color: #e8d5b7;
border: 1px solid #d4af37;
border-radius: 4px;
}
.logs {
margin-top: 1rem;
padding: 0.5rem;
background: rgba(0,0,0,0.3);
border-radius: 4px;
}
.logs h3 {
margin: 0 0 0.5rem 0;
font-size: 0.9rem;
color: #8b7355;
}
.logs p {
margin: 0.25rem 0;
font-size: 0.85rem;
font-family: monospace;
}
.hint {
font-size: 0.85rem;
color: #8b7355;
font-style: italic;
margin-top: 0.5rem;
}
strong {
color: #d4af37;
font-size: 1.5rem;
}
:global(body.dark) {
background: #0a0a0a;
}
:global(body.light) {
background: #f5f5f5;
}
</style>Docs entries used in this lesson
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is $effect used for?
2. How do you perform cleanup in $effect?
Hands-on tasks in the game
- Code editor
Complete the code to log the value of count on every change
- Click in order
Click the good use cases for $effect in this order: localStorage, external libraries, logging