Kurs Svelte i SvelteKit · Moduł 7: Cykl życia

Aktualizacje DOM: $effect.pre, $effect i tick()

3 min czytania ·
W tej lekcji5

Czasem trzeba zareagować tuż przed zmianą DOM albo zaraz po niej - na przykład przewinąć czat wojowników do najnowszej wiadomości. W Svelte 5 służą do tego dwie runy.

$effect.pre - przed aktualizacją DOM

$effect.pre uruchamia się przed aktualizacją DOM, za każdym razem gdy zmieni się stan, który czyta:

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

  $effect.pre(() => {
    console.log('Przed aktualizacją DOM, wiadomości:', messages.length);
  });
</script>

$effect - po aktualizacji DOM

Zwykły $effect działa po aktualizacji DOM, więc może już mierzyć i przewijać elementy:

<script>
  let messages = $state(['Witaj w Walhalli!']);
  let container;

  $effect(() => {
    messages.length; // zależność: efekt wróci po każdej nowej wiadomości

    // Scroll do dołu po dodaniu wiadomości
    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!')}>Dodaj wiadomość</button>

Linijka messages.length; tylko odczytuje stan, żeby efekt wiedział, od czego zależy. Na inne zmiany w komponencie (np. przełączenie motywu) ten efekt nie reaguje.

Nie zmieniaj w efekcie stanu, który on czyta

Efekt, który czyta log i jednocześnie go zmienia, uruchomi się znowu, i znowu... Svelte przerwie taką pętlę błędem effect_update_depth_exceeded. Wartości wyliczane ze stanu zapisuj przez $derived, a gdy efekt naprawdę musi coś zapisać, owiń ten zapis w untrack:

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

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

  $effect(() => {
    const value = count; // jedyna zależność efektu

    // untrack: odczyt log wewnątrz nie staje się zależnością
    untrack(() => {
      log = [...log, `count = ${value}`];
    });
  });
</script>

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

tick() - Poczekaj na aktualizację DOM

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

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

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

    // Poczekaj aż DOM się zaktualizuje
    await tick();

    // Teraz możemy bezpiecznie operować na DOM
    input.setSelectionRange(text.length, text.length);
  }
</script>

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

Svelte 4 (legacy): beforeUpdate i afterUpdate

W starszym kodzie zobaczysz beforeUpdate i afterUpdate. Uruchamiały się przy każdej aktualizacji komponentu. W Svelte 5 są przestarzałe i działają tylko w komponentach bez run - w komponencie z $state ich import to błąd kompilacji. Zamieniaj je tak:

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>

Efekty są dokładniejsze od starych hooków: reagują tylko na stan, którego używają, a nie na każdą zmianę w komponencie.

Kod do tej lekcji: App.svelte
<script>
  // $effect - nowy sposób na efekty w Svelte 5
  // Zastępuje beforeUpdate i afterUpdate (onMount nadal działa)
  import { untrack } from 'svelte';

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

  // untrack: zapis do logs nie staje się zależnością efektu,
  // który wywołał log() - inaczej efekt czytałby i zmieniał logs w kółko
  function log(msg) {
    untrack(() => {
      logs = [...logs, msg].slice(-10);
    });
  }

  // $effect uruchamia się gdy zależności się zmienią
  $effect(() => {
    log(`Effect: count = ${count}`);
  });

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

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

    // Cleanup - zwróć funkcję
    return () => {
      clearInterval(interval);
      log('Cleanup interval');
    };
  });

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

<main class="effects">
  <h1>$effect - Reaktywne efekty</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>Witaj, <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>

Hasła z dokumentacji w tej lekcji

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Do czego służy funkcja tick()?

  2. 2. Czym jest $effect.pre w Svelte 5?

Przydatne artykuły