Перейти к содержимому

8. Lifecycle хуки

Каждый компонент живёт своей жизнью: рождается, обновляется и умирает. 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(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! 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 НЕ запускается на сервере! Это сделано специально — хук предназначен только для браузерного кода.

<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 ✅ afterUpdate

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>
<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(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(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() — это не 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 — вот таблица соответствий:

SvelteReactКогда
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!
}, []);

<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}
<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>
<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>
<script>
import { onMount } from 'svelte';
import { userStore } from './stores';
let user = null;
onMount(() => {
// Ручная подписка (обычно используют $userStore)
const unsubscribe = userStore.subscribe(value => {
user = value;
});
return unsubscribe; // Отписываемся при уничтожении!
});
</script>
<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>

Хуки работают для компонента, в котором вызваны. Можно создавать переиспользуемую логику:

<!-- 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 появились 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() [не изменился!]

<!-- ✅ Правильно -->
<script>
import { onMount } from 'svelte';
onMount(() => console.log('OK'));
</script>
<!-- ❌ Неправильно — нельзя вызывать в функциях/условиях -->
<script>
function setup() {
onMount(() => {}); // Не работает!
}
</script>
<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>
<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

Монтируй и размонтируй компонент, запускай таймер, следи за часами — и наблюдай, когда именно срабатывает каждый lifecycle-хук!