8. Lifecycle хуки
Lifecycle в Svelte: onMount, onDestroy и друзья 🔄
Заголовок раздела «Lifecycle в Svelte: onMount, onDestroy и друзья 🔄»Каждый компонент живёт своей жизнью: рождается, обновляется и умирает. Svelte даёт нам хуки, чтобы “подцепиться” к каждому из этих моментов. Если ты знаком с React — сравни с useEffect, и многое станет на своё место! 😎
Обзор: все хуки в одном взгляде 👀
Заголовок раздела «Обзор: все хуки в одном взгляде 👀»Svelte предоставляет четыре lifecycle-хука плюс специальную функцию tick():
Компонент создаётся │ onMount() ✅ ← DOM готов! Самый частый хук │ ┌──────┴──────┐ │ Данные │ │ изменились │ └──────┬──────┘ │ beforeUpdate() → обновление DOM → afterUpdate() │ Компонент уничтожается │ onDestroy() 🧹 ← чистим ресурсыВсе хуки импортируются из 'svelte':
<script> import { onMount, onDestroy, beforeUpdate, afterUpdate, tick } from 'svelte';</script>💡 В Svelte 5 все эти хуки заменяются на
$effect()— об этом в конце урока!
onMount — главный хук 🚀
Заголовок раздела «onMount — главный хук 🚀»onMount(callback) запускается после того, как компонент вставлен в DOM. Это аналог useEffect(fn, []) в React — выполняется один раз после первого рендера.
<script> import { onMount } from 'svelte';
let users = []; let loading = true;
onMount(async () => { // ✅ DOM уже готов — можно работать с элементами // ✅ Запрашиваем данные const res = await fetch('https://jsonplaceholder.typicode.com/users'); users = await res.json(); loading = false; });</script>
{#if loading} <p>Загрузка...</p>{:else} <ul> {#each users as user} <li>{user.name}</li> {/each} </ul>{/if}Идеально подходит для:
Заголовок раздела «Идеально подходит для:»| Задача | Пример |
|---|---|
| Загрузка данных | fetch('/api/data') |
| Инициализация библиотек | new Chart(canvas, {...}) |
| Запуск таймеров | setInterval(fn, 1000) |
| Подписка на события | window.addEventListener(...) |
| Работа с DOM-элементами | element.focus() |
onMount + cleanup = магия 🪄
Заголовок раздела «onMount + cleanup = магия 🪄»Самая крутая фишка: можно вернуть функцию очистки прямо из onMount! Svelte вызовет её при уничтожении компонента — как useEffect(() => { return cleanup }, []) в React:
<script> import { onMount } from 'svelte';
let seconds = 0;
onMount(() => { // Запускаем таймер при монтировании const interval = setInterval(() => { seconds += 1; }, 1000);
// Возвращаем функцию очистки — она вызовется при onDestroy! return () => { clearInterval(interval); console.log('Таймер остановлен ✅'); }; });</script>
<p>Компонент живёт уже: {seconds} сек.</p>💡 Если вернуть cleanup из
onMount— отдельныйonDestroyне нужен! Но оба варианта работают.
onMount и SSR ⚠️
Заголовок раздела «onMount и SSR ⚠️»Очень важный момент: onMount НЕ запускается на сервере! Это сделано специально — хук предназначен только для браузерного кода.
<script> import { onMount } from 'svelte';
let mapInstance = null;
onMount(() => { // Безопасно! Этот код никогда не запустится на сервере. // Библиотека Leaflet работает только в браузере (нужен window/document) import('leaflet').then(L => { mapInstance = L.map('map').setView([55.75, 37.61], 13); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(mapInstance); });
return () => mapInstance?.remove(); });</script>
<div id="map" style="height: 400px;" />Что запускается при SSR, а что нет:
При SSR (сервер): В браузере:───────────────── ────────────────✅ <script> тело ✅ <script> тело✅ Реактивные $: ✅ Реактивные $:❌ onMount ✅ onMount✅ onDestroy ✅ onDestroy❌ beforeUpdate ✅ beforeUpdate❌ afterUpdate ✅ afterUpdateonDestroy — убираем за собой 🧹
Заголовок раздела «onDestroy — убираем за собой 🧹»onDestroy(callback) вызывается перед уничтожением компонента. Используй его для очистки ресурсов, которые иначе останутся “висеть” в памяти:
<script> import { onMount, onDestroy } from 'svelte';
let position = { x: 0, y: 0 };
function handleMouseMove(event) { position = { x: event.clientX, y: event.clientY }; }
onMount(() => { window.addEventListener('mousemove', handleMouseMove); });
// Убираем слушатель, когда компонент удалён из DOM onDestroy(() => { window.removeEventListener('mousemove', handleMouseMove); console.log('Компонент уничтожен, слушатель удалён 🧹'); });</script>
<p>Мышь: x={position.x}, y={position.y}</p>Типичные паттерны cleanup:
Заголовок раздела «Типичные паттерны cleanup:»<script> import { onMount, onDestroy } from 'svelte';
// Паттерн 1: таймер let timer; onMount(() => { timer = setInterval(tick, 1000); }); onDestroy(() => clearInterval(timer));
// Паттерн 2: WebSocket let ws; onMount(() => { ws = new WebSocket('wss://example.com'); }); onDestroy(() => ws?.close());
// Паттерн 3: ResizeObserver let observer; let el; onMount(() => { observer = new ResizeObserver(entries => { console.log('Размер изменился:', entries[0].contentRect); }); observer.observe(el); }); onDestroy(() => observer?.disconnect());</script>beforeUpdate — перед обновлением DOM 🔮
Заголовок раздела «beforeUpdate — перед обновлением DOM 🔮»beforeUpdate(callback) вызывается прямо перед тем, как Svelte обновит DOM в ответ на изменение состояния. Важно: он вызывается и перед первым рендером (до onMount)!
<script> import { beforeUpdate } from 'svelte';
let messages = []; let div; let autoscroll = false;
beforeUpdate(() => { // Сохраняем, нужно ли прокручивать ПЕРЕД обновлением // (потом в afterUpdate — прокручиваем) if (div) { const scrollableDistance = div.scrollHeight - div.offsetHeight; autoscroll = div.scrollTop > scrollableDistance - 20; } });</script>⚠️ Не изменяй реактивные данные в
beforeUpdate— получишь бесконечный цикл!
afterUpdate — после обновления DOM ✅
Заголовок раздела «afterUpdate — после обновления DOM ✅»afterUpdate(callback) вызывается после каждого обновления DOM. Классический паттерн — автоскролл в чате:
<script> import { beforeUpdate, afterUpdate } from 'svelte';
let messages = ['Привет!', 'Как дела?']; let div; let autoscroll = false;
beforeUpdate(() => { if (div) { const scrollableDistance = div.scrollHeight - div.offsetHeight; autoscroll = div.scrollTop > scrollableDistance - 20; } });
afterUpdate(() => { // Прокручиваем вниз только если пользователь уже был внизу if (autoscroll) { div.scrollTo(0, div.scrollHeight); } });
function sendMessage() { messages = [...messages, `Сообщение ${messages.length + 1}`]; }</script>
<div bind:this={div} style="height: 200px; overflow-y: auto;"> {#each messages as message} <p>{message}</p> {/each}</div>
<button on:click={sendMessage}>Отправить</button>tick() — ждём обновления DOM ⏱️
Заголовок раздела «tick() — ждём обновления DOM ⏱️»tick() — это не lifecycle-хук, а утилита, которая возвращает Promise. Он резолвится после того, как все ожидающие изменения состояния применены к DOM. Аналог flushSync в React или nextTick во Vue:
<script> import { tick } from 'svelte';
let text = 'Привет!'; let input;
async function handleClick() { text = 'Обновлённый текст';
// DOM ещё НЕ обновлён здесь! // input.selectionStart — было бы неправильное значение
await tick(); // Ждём, пока Svelte обновит DOM
// Теперь DOM актуален input.selectionStart = 0; input.selectionEnd = input.value.length; input.focus(); }</script>
<input bind:this={input} bind:value={text} /><button on:click={handleClick}>Обновить и выделить</button>Ещё один пример tick() — автоформатирование:
Заголовок раздела «Ещё один пример tick() — автоформатирование:»<script> import { tick } from 'svelte';
let code = ''; let textarea;
async function handleInput() { const before = textarea.selectionStart;
// Форматируем текст code = code.toUpperCase();
await tick();
// Восстанавливаем позицию курсора после изменения textarea.selectionStart = before; textarea.selectionEnd = before; }</script>
<textarea bind:this={textarea} bind:value={code} on:input={handleInput}/>Полный порядок событий 📋
Заголовок раздела «Полный порядок событий 📋»Посмотрим, в каком порядке всё срабатывает:
Компонент создаётся: 1. beforeUpdate() ← первый вызов, DOM ещё нет! 2. [рендер DOM] 3. onMount() ← DOM готов
Состояние изменяется: 4. beforeUpdate() 5. [обновление DOM] 6. afterUpdate()
Компонент уничтожается: 7. onDestroy() ← (или cleanup из onMount)Сравнение с React useEffect 🔄
Заголовок раздела «Сравнение с React useEffect 🔄»Если ты приходишь из React — вот таблица соответствий:
| Svelte | React | Когда |
|---|---|---|
onMount(fn) | useEffect(fn, []) | Один раз после монтирования |
onDestroy(fn) | useEffect(() => fn, []) | При размонтировании |
beforeUpdate(fn) | useEffect с deps | Перед обновлением DOM |
afterUpdate(fn) | useEffect с deps | После обновления DOM |
tick() | flushSync() или setTimeout(fn, 0) | Ждём обновления DOM |
<!-- Svelte: onMount с cleanup --><script> import { onMount } from 'svelte';
onMount(() => { const id = setInterval(() => console.log('tick'), 1000); return () => clearInterval(id); // cleanup! });</script>// React: аналогuseEffect(() => { const id = setInterval(() => console.log('tick'), 1000); return () => clearInterval(id); // cleanup!}, []);Практические паттерны 🛠️
Заголовок раздела «Практические паттерны 🛠️»Паттерн 1: Fetch с AbortController
Заголовок раздела «Паттерн 1: Fetch с AbortController»<script> import { onMount } from 'svelte';
let data = null; let error = null;
onMount(() => { const controller = new AbortController();
fetch('/api/data', { signal: controller.signal }) .then(res => res.json()) .then(json => { data = json; }) .catch(err => { if (err.name !== 'AbortError') error = err.message; });
// Отменяем запрос при уничтожении компонента! return () => controller.abort(); });</script>
{#if error}<p style="color: red">{error}</p>{/if}{#if data}<pre>{JSON.stringify(data, null, 2)}</pre>{/if}Паттерн 2: ResizeObserver
Заголовок раздела «Паттерн 2: ResizeObserver»<script> import { onMount } from 'svelte';
let width = 0; let height = 0; let box;
onMount(() => { const ro = new ResizeObserver(([entry]) => { ({ width, height } = entry.contentRect); });
ro.observe(box); return () => ro.disconnect(); });</script>
<div bind:this={box} style="resize: both; overflow: auto; padding: 1rem; border: 1px solid #ccc;"> Я {Math.round(width)}×{Math.round(height)} пикселей!</div>Паттерн 3: IntersectionObserver
Заголовок раздела «Паттерн 3: IntersectionObserver»<script> import { onMount } from 'svelte';
let visible = false; let el;
onMount(() => { const io = new IntersectionObserver(([entry]) => { visible = entry.isIntersecting; }, { threshold: 0.5 });
io.observe(el); return () => io.disconnect(); });</script>
<div style="height: 200vh; display: flex; align-items: center;"> <div bind:this={el} style="padding: 2rem; background: {visible ? '#4ade80' : '#f87171'}; transition: background 0.3s;" > Я {visible ? 'видим' : 'не видим'}! </div></div>Паттерн 4: Подписка на Store в onMount
Заголовок раздела «Паттерн 4: Подписка на Store в onMount»<script> import { onMount } from 'svelte'; import { userStore } from './stores';
let user = null;
onMount(() => { // Ручная подписка (обычно используют $userStore) const unsubscribe = userStore.subscribe(value => { user = value; });
return unsubscribe; // Отписываемся при уничтожении! });</script>Паттерн 5: Event Listener с cleanup
Заголовок раздела «Паттерн 5: Event Listener с cleanup»<script> import { onMount } from 'svelte';
let keys = [];
onMount(() => { const handler = (e) => { keys = [...keys.slice(-9), e.key]; };
window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); });</script>
<p>Последние нажатия: {keys.join(' → ')}</p>Lifecycle внутри компонентов-обёрток 📦
Заголовок раздела «Lifecycle внутри компонентов-обёрток 📦»Хуки работают для компонента, в котором вызваны. Можно создавать переиспользуемую логику:
<!-- useTimer.js — переиспользуемая логика (Svelte 4) --><script context="module"> import { onMount, onDestroy } from 'svelte'; import { writable } from 'svelte/store';
export function createTimer() { const seconds = writable(0); let interval;
onMount(() => { interval = setInterval(() => seconds.update(n => n + 1), 1000); return () => clearInterval(interval); });
return seconds; }</script>Svelte 5: $effect() заменяет всё 🆕
Заголовок раздела «Svelte 5: $effect() заменяет всё 🆕»В Svelte 5 появились runes — новая система реактивности. Все lifecycle-хуки заменяются на $effect():
<!-- Svelte 5 --><script> let count = $state(0);
$effect(() => { // Запускается после монтирования и при каждом изменении зависимостей const interval = setInterval(() => count++, 1000);
// Возвращаем cleanup — работает как onMount + onDestroy return () => clearInterval(interval); });
// $effect.pre — аналог beforeUpdate $effect.pre(() => { console.log('Перед обновлением DOM, count =', count); });</script>
<p>Count: {count}</p>Сравнение Svelte 4 vs Svelte 5:
Svelte 4 Svelte 5───────────── ──────────────────────onMount(fn) → $effect(fn) [с cleanup]onDestroy(fn) → $effect(() => fn) [return fn]beforeUpdate → $effect.pre(fn)afterUpdate → $effect(fn) [без cleanup]tick() → tick() [не изменился!]Важные правила и ловушки ⚠️
Заголовок раздела «Важные правила и ловушки ⚠️»Правило 1: Хуки вызываются только в <script>
Заголовок раздела «Правило 1: Хуки вызываются только в <script>»<!-- ✅ Правильно --><script> import { onMount } from 'svelte'; onMount(() => console.log('OK'));</script>
<!-- ❌ Неправильно — нельзя вызывать в функциях/условиях --><script> function setup() { onMount(() => {}); // Не работает! }</script>Правило 2: async onMount — осторожно!
Заголовок раздела «Правило 2: async onMount — осторожно!»<script> import { onMount } from 'svelte';
onMount(async () => { const data = await fetch('/api').then(r => r.json()); // ...
// ⚠️ НЕЛЬЗЯ вернуть cleanup из async функции! // async функция возвращает Promise, а не функцию });
// ✅ Правильный паттерн с cleanup: onMount(() => { let cancelled = false;
fetch('/api').then(r => r.json()).then(data => { if (!cancelled) { /* обновляем состояние */ } });
return () => { cancelled = true; }; });</script>Правило 3: Порядок имеет значение
Заголовок раздела «Правило 3: Порядок имеет значение»<script> import { beforeUpdate, afterUpdate, onMount } from 'svelte';
// ⚠️ beforeUpdate вызывается ПЕРЕД первым рендером тоже! beforeUpdate(() => { // При первом вызове DOM ещё не существует! // Проверяй, что элемент существует перед работой с ним });</script>Итоги урока 🎯
Заголовок раздела «Итоги урока 🎯»| Хук | Когда | Зачем |
|---|---|---|
onMount(fn) | После монтирования | API, библиотеки, таймеры |
onMount(() => cleanup) | Mount + Destroy | Таймеры с очисткой |
onDestroy(fn) | Перед уничтожением | Отписки, закрытие соединений |
beforeUpdate(fn) | Перед обновлением DOM | Сохранить позицию скролла |
afterUpdate(fn) | После обновления DOM | Автоскролл в чате |
tick() | По требованию | Дождаться обновления DOM |
Интерактивный Playground 🎮
Заголовок раздела «Интерактивный Playground 🎮»Монтируй и размонтируй компонент, запускай таймер, следи за часами — и наблюдай, когда именно срабатывает каждый lifecycle-хук!