Side effects with $effect

Svelte · Practical · 12 min

Effects connect Svelte state to the outside world: canvases, timers, third-party libraries, localStorage.

`$effect(fn)` runs fn after the component is mounted and again whenever state it read changes, after the DOM has been updated. It is for side effects: drawing on a canvas, syncing to localStorage, setting up a library, logging analytics.

$effect(() => {
  const id = setInterval(() => elapsed++, interval);
  return () => clearInterval(id);    // teardown
});
  • Dependencies are tracked automatically: any $state, $derived or prop read synchronously inside the function. Values read after an await or inside a setTimeout callback are not tracked.
  • If the function returns a teardown function, it runs before the effect re-runs and when the component is destroyed.
  • Effects run only in the browser, never during server-side rendering.
  • Use untrack(() => value) to read something without depending on it.
  • $effect.pre runs before the DOM update (for example to preserve scroll position).
  • Svelte batches changes, so the effect runs once even if several dependencies changed.

When not to use an effect: to compute a value from state (use $derived), or to respond to a user action (do it in the event handler). Writing state inside an effect that also reads it can create loops; Svelte throws effect_update_depth_exceeded when an effect keeps re-triggering itself.

For one-time setup on mount, an effect that reads no state runs once; onMount from svelte also still works.

Example

<script>
  let interval = $state(1000);
  let elapsed = $state(0);
  let canvas;

  $effect(() => {
    const id = setInterval(() => elapsed++, interval);  // depends on interval
    return () => clearInterval(id);                     // cleanup before re-run
  });

  $effect(() => {
    const ctx = canvas.getContext('2d');
    ctx.clearRect(0, 0, 200, 20);
    ctx.fillRect(0, 0, (elapsed % 10) * 20, 20);        // redraw when elapsed changes
  });

  $effect(() => {
    localStorage.setItem('interval', String(interval)); // sync to storage
  });
</script>

<canvas bind:this={canvas} width="200" height="20"></canvas>
<p>{elapsed} ticks</p>
<button onclick={() => (interval /= 2)}>faster</button>

Remember

  • $effect runs after mount and after DOM updates when its tracked state changes
  • Return a teardown function to clean up timers and listeners
  • Use $derived for values and event handlers for user actions; effects are an escape hatch

Watch out for

  • Reading state after an await inside an effect and expecting it to be a dependency.

Check yourself

  1. An effect sets `doubled = count * 2` where doubled is $state. What should you use instead?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: svelte.dev/docs/svelte/$effect