Svelte and SvelteKit course · Module 7: Lifecycle

DOM Updates: $effect.pre, $effect and tick()

3 min read ·
In this lesson5

Sometimes you need to react right before the DOM changes or right after - for example, to scroll the warriors' chat to the newest message. Svelte 5 has two runes for this.

$effect.pre - Before the DOM Update

$effect.pre runs before the DOM update, every time the state it reads changes:

<script>
  let messages = $state([]);

  $effect.pre(() => {
    console.log('Before the DOM update, messages:', messages.length);
  });
</script>

$effect - After the DOM Update

A regular $effect runs after the DOM update, so it can already measure and scroll elements:

<script>
  let messages = $state(['Welcome to Valhalla!']);
  let container;

  $effect(() => {
    messages.length; // dependency: the effect returns after every new message

    // Scroll to the bottom after a message is added
    container.scrollTop = container.scrollHeight;
  });
</script>

<div bind:this={container} class="messages">
  {#each messages as msg}
    <p>{msg}</p>
  {/each}
</div>
<button onclick={() => messages.push('SKÅL!')}>Add message</button>

The line messages.length; only reads the state, so the effect knows what it depends on. This effect ignores other changes in the component (such as switching the theme).

Do Not Change the State an Effect Reads

An effect that reads log and changes it at the same time runs again, and again... Svelte stops such a loop with the effect_update_depth_exceeded error. Store values computed from state with $derived, and when an effect really has to write something, wrap that write in untrack:

<script>
  import { untrack } from 'svelte';

  let count = $state(0);
  let log = $state([]);

  $effect(() => {
    const value = count; // the effect's only dependency

    // untrack: reading log inside does not become a dependency
    untrack(() => {
      log = [...log, `count = ${value}`];
    });
  });
</script>

<button onclick={() => count++}>+1</button>
<p>{log.join(', ')}</p>

tick() - Wait for DOM Update

<script>
  import { tick } from 'svelte';

  let text = $state('');
  let input;

  async function addText(newText) {
    text += newText;

    // Wait until the DOM updates
    await tick();

    // Now we can safely work with the DOM
    input.setSelectionRange(text.length, text.length);
  }
</script>

<input bind:this={input} bind:value={text} />
<button onclick={() => addText(' Viking!')}>Add</button>

Svelte 4 (legacy): beforeUpdate and afterUpdate

In older code you will see beforeUpdate and afterUpdate. They ran on every update of the component. In Svelte 5 they are deprecated and work only in components without runes - in a component with $state, importing them is a compile error. Replace them like this:

Svelte 4Svelte 5
beforeUpdate(fn)$effect.pre(fn)
afterUpdate(fn)$effect(fn)
<!-- Chat.svelte - Svelte 4 (legacy) -->
<script>
  import { afterUpdate } from 'svelte';

  export let messages = [];
  let container;

  afterUpdate(() => {
    container.scrollTop = container.scrollHeight;
  });
</script>

<div bind:this={container}>
  {#each messages as msg}
    <p>{msg}</p>
  {/each}
</div>

Effects are more precise than the old hooks: they react only to the state they use, not to every change in the component.

Code for this lesson: App.svelte
<script>
  // $effect - new way to handle effects in Svelte 5
  // Replaces beforeUpdate and afterUpdate (onMount still works)
  import { untrack } from 'svelte';

  let count = $state(0);
  let name = $state('Ragnar');
  let logs = $state([]);

  // untrack: writing to logs does not become a dependency of the effect
  // that called log() - otherwise the effect would read and change logs forever
  function log(msg) {
    untrack(() => {
      logs = [...logs, msg].slice(-10);
    });
  }

  // $effect runs when dependencies change
  $effect(() => {
    log(`Effect: count = ${count}`);
  });

  $effect(() => {
    log(`Effect: name = ${name}`);
  });

  // $effect with cleanup
  $effect(() => {
    const interval = setInterval(() => {
      log('⏰ Tick...');
    }, 3000);

    // Cleanup - return a function
    return () => {
      clearInterval(interval);
      log('Cleanup interval');
    };
  });

  // $effect.pre - before DOM update
  $effect.pre(() => {
    if (count > 0) {
      log(`⏳ Pre-effect: count will be ${count}`);
    }
  });
</script>

<main class="effects">
  <h1>$effect - Reactive effects</h1>

  <section>
    <h2>Counter</h2>
    <p>Count: <strong>{count}</strong></p>
    <button onclick={() => count++}>+1</button>
  </section>

  <section>
    <h2>Name</h2>
    <input bind:value={name} />
    <p>Hello, <strong>{name}</strong>!</p>
  </section>

  <section class="logs">
    <h2>Effect logs</h2>
    {#each logs as entry, i}
      <p>{entry}</p>
    {/each}
  </section>
</main>

<style>
  .effects {
    font-family: 'Georgia', serif;
    max-width: 500px;
    margin: 0 auto;
    padding: 2rem;
    background: #1a0f0a;
    color: #e8d5b7;
    min-height: 100vh;
  }

  h1 {
    color: #d4af37;
    text-align: center;
  }

  section {
    background: rgba(212, 175, 55, 0.1);
    border: 1px solid #d4af37;
    border-radius: 8px;
    padding: 1rem;
    margin: 1rem 0;
  }

  h2 {
    color: #d4af37;
    font-size: 1rem;
    margin-top: 0;
  }

  strong {
    color: #ffd700;
  }

  button {
    padding: 0.5rem 1rem;
    background: #3d2914;
    color: #e8d5b7;
    border: 1px solid #d4af37;
    border-radius: 4px;
    cursor: pointer;
  }

  input {
    width: 100%;
    padding: 0.5rem;
    background: rgba(0,0,0,0.3);
    border: 1px solid #3d2914;
    border-radius: 4px;
    color: #e8d5b7;
    margin-bottom: 0.5rem;
  }

  .logs {
    max-height: 200px;
    overflow-y: auto;
  }

  .logs p {
    margin: 0.25rem 0;
    padding: 0.25rem 0.5rem;
    background: rgba(0,0,0,0.3);
    border-radius: 4px;
    font-size: 0.85rem;
    font-family: monospace;
  }
</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 the tick() function used for?

  2. 2. What is $effect.pre in Svelte 5?

Useful articles