Svelte and SvelteKit course · Module 3: Reactivity and Runes

$effect - Side Effects

4 min read ·
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 load in 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. 1. What is $effect used for?

  2. 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

Useful articles