Kurs Svelte i SvelteKit · Moduł 7: Cykl życia
Aktualizacje DOM: $effect.pre, $effect i tick()
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 4 | Svelte 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. Do czego służy funkcja tick()?
2. Czym jest $effect.pre w Svelte 5?