Kurs Svelte i SvelteKit · Moduł 7: Cykl życia
Praktyczne wzorce lifecycle
W tej lekcji4
Auto-save
<script>
import { onDestroy } from 'svelte';
let content = $state('');
// Debounced auto-save
$effect(() => {
const draft = content; // odczyt = zależność efektu
const timeout = setTimeout(() => {
saveToServer(draft);
}, 1000);
// Sprzątanie przed kolejnym uruchomieniem anuluje poprzedni zapis
return () => clearTimeout(timeout);
});
onDestroy(() => {
// Zapisz przed zamknięciem
saveToServer(content);
});
async function saveToServer(data) {
await fetch('/api/save', {
method: 'POST',
body: JSON.stringify({ content: data })
});
}
</script>
<textarea bind:value={content}></textarea>Efekt śledzi tylko stan odczytany od razu, w jego ciele. Gdyby content był czytany dopiero w setTimeout, efekt nie wiedziałby, że od niego zależy, i nie wróciłby po kolejnej zmianie tekstu.
Subscription cleanup
<script>
import { onMount } from 'svelte';
let position = $state({ x: 0, y: 0 });
onMount(() => {
function handleMouseMove(e) {
position = { x: e.clientX, y: e.clientY };
}
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
});
</script>
<p>Pozycja: {position.x}, {position.y}</p>Intersection Observer
<script>
let element;
let isVisible = $state(false);
$effect(() => {
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
isVisible = entry.isIntersecting;
},
{ threshold: 0.5 }
);
observer.observe(element);
return () => observer.disconnect();
});
</script>
<div bind:this={element} class:visible={isVisible}>
{isVisible ? 'Widoczny!' : 'Niewidoczny'}
</div>SSR i onMount
W SvelteKit komponent renderuje się najpierw na serwerze, gdzie nie ma window ani localStorage. onMount i $effect nie uruchamiają się na serwerze, ale kod na najwyższym poziomie <script> - już tak:
<script>
import { onMount } from 'svelte';
import { browser } from '$app/env'; // SvelteKit 3 (w SvelteKit 2: '$app/environment')
let windowWidth = $state(0);
// onMount NIE uruchamia się na serwerze
onMount(() => {
windowWidth = window.innerWidth;
});
// Kod na najwyższym poziomie działa też podczas SSR,
// więc API przeglądarki zabezpiecz flagą browser
const savedName = browser ? localStorage.getItem('viking-name') : null;
</script>Dostęp do API przeglądarki umieszczaj więc w onMount albo w $effect, a poza nimi sprawdzaj browser.
Kod do tej lekcji: App.svelte
<script>
// tick() - czekanie na aktualizację DOM
import { tick } from 'svelte';
let messages = $state([
'Witaj w Walhalli!',
'Tutaj wojownicy odpoczywają'
]);
let inputValue = $state('');
let listElement;
async function addMessage() {
if (!inputValue.trim()) return;
messages = [...messages, inputValue];
inputValue = '';
// Czekaj na aktualizację DOM
await tick();
// Teraz DOM jest zaktualizowany
listElement?.scrollTo({
top: listElement.scrollHeight,
behavior: 'smooth'
});
}
// Przykład z tekstem
let text = $state('Zmień mnie');
let selection = $state({ start: 0, end: 0 });
let textareaElement;
async function transformText() {
// Zapisz pozycję kursora
const start = textareaElement.selectionStart;
const end = textareaElement.selectionEnd;
// Zmień tekst na wielkie litery
text = text.toUpperCase();
// Czekaj na update DOM
await tick();
// Przywróć pozycję kursora
textareaElement.selectionStart = start;
textareaElement.selectionEnd = end;
}
</script>
<main class="tick-demo">
<h1>⏳ tick() - Synchronizacja z DOM</h1>
<section>
<h2>Auto-scroll do końca listy</h2>
<div class="messages" bind:this={listElement}>
{#each messages as msg}
<p>{msg}</p>
{/each}
</div>
<div class="input-row">
<input
bind:value={inputValue}
placeholder="Napisz wiadomość..."
onkeydown={(e) => e.key === 'Enter' && addMessage()}
/>
<button onclick={addMessage}>Wyślij</button>
</div>
</section>
<section>
<h2>Zachowanie pozycji kursora</h2>
<textarea
bind:this={textareaElement}
bind:value={text}
rows="3"
></textarea>
<button onclick={transformText}>WIELKIE LITERY</button>
<p class="hint">Kursor pozostanie w tym samym miejscu po transformacji</p>
</section>
</main>
<style>
.tick-demo {
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;
}
.messages {
height: 150px;
overflow-y: auto;
background: rgba(0,0,0,0.3);
border-radius: 4px;
padding: 0.5rem;
margin-bottom: 0.5rem;
}
.messages p {
margin: 0.25rem 0;
padding: 0.5rem;
background: rgba(212, 175, 55, 0.1);
border-radius: 4px;
}
.input-row {
display: flex;
gap: 0.5rem;
}
input, textarea {
flex: 1;
padding: 0.5rem;
background: rgba(0,0,0,0.3);
border: 1px solid #3d2914;
border-radius: 4px;
color: #e8d5b7;
}
textarea {
width: 100%;
resize: vertical;
margin-bottom: 0.5rem;
}
button {
padding: 0.5rem 1rem;
background: #3d2914;
color: #e8d5b7;
border: 1px solid #d4af37;
border-radius: 4px;
cursor: pointer;
}
.hint {
font-size: 0.85rem;
color: #8b7355;
font-style: italic;
margin-top: 0.5rem;
}
</style>Hasła z dokumentacji w tej lekcji
Widzisz błąd w tej lekcji?
Zadania praktyczne w grze
- Edytor kodu
Uzupełnij kod, aby usunąć event listener przy odmontowaniu