Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Side effects: $effect

A side effect is an action that reaches outside the component's markup: changing the tab title, writing data to localStorage, starting a timer. Such actions use the $effect rune: it runs a function after the component appears on the page and runs it again when state read inside it changes.

The first effect

<!-- src/routes/+page.svelte -->
<script>
  let todos = $state([{ id: 1, text: 'Buy bread', done: false }]);
 
  $effect(() => {
    document.title = `Tasks: ${todos.length}`;
  });
</script>
 
<button onclick={() => todos.push({ id: Date.now(), text: 'New', done: false })}>
  Add
</button>

The browser tab gets the title "Tasks: 1", and the number grows after each click. Dependencies are not listed: Svelte sees that the function reads todos.length.

Saving to localStorage

An effect is handy for keeping the to-do list between reloads:

<!-- src/routes/+page.svelte -->
<script>
  let todos = $state([]);
 
  // 1. Read the saved list once
  $effect(() => {
    todos = JSON.parse(localStorage.getItem('todos') ?? '[]');
  });
 
  // 2. Save the list on every change
  $effect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  });
</script>
 
<p>Tasks: {todos.length}</p>
<button onclick={() => todos.push({ id: Date.now(), text: 'New', done: false })}>
  Add
</button>

Add a couple of tasks and reload the page — the number stays. Effects run in order and only in the browser, so using localStorage inside them is safe. The code at the top of <script>, however, also runs on the server in SvelteKit, where there is no localStorage — that is why the list is read in an effect rather than in $state(...). The first effect only writes todos and reads no state, so it does not run again.

Note that JSON.stringify walks the whole array, so the effect reruns on any change inside it.

Cleanup

If an effect starts something long-lived — a timer, a subscription — return a cleanup function from it. Svelte calls it before the effect reruns and when the component is removed:

<script>
  let seconds = $state(0);
 
  $effect(() => {
    const timer = setInterval(() => seconds++, 1000);
    return () => clearInterval(timer);
  });
</script>
 
<p>You have been here for {seconds} s</p>

The number on the page grows every second. When the component disappears, the timer stops and does not run in vain.

When you do not need an effect

For values computed from state use $derived, and to react to a click use an event handler. $effect is only for talking to the outside world: the DOM, browser storage, third-party libraries.

Common mistake

Changing the same state the effect reads:

<script>
  let count = $state(0);
 
  $effect(() => {
    count = count + 1; // the effect restarts itself
  });
</script>

The effect changed count, which caused it to run again, and so on forever — Svelte stops the loop with an error. If you need a new value from an old one, that is a job for $derived or an event handler.

Thanks for reading!

Donate to the site — we work hard for you.