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

9. Lifecycle: onMount, onCleanup

Solid принципиально отличается от React отсутствием «жизненного цикла компонента». Компоненты в Solid — это просто функции, которые вызываются один раз. Вся реактивность — через примитивы: createEffect, onMount, onCleanup.

В React есть методы жизненного цикла: componentDidMount, componentDidUpdate, componentWillUnmount. В Solid их нет. Вместо них — три примитива:

import { createEffect, onMount, onCleanup } from 'solid-js';
function MyComponent() {
// 1. onMount — аналог componentDidMount, запускается ОДИН РАЗ
onMount(() => {
console.log('DOM смонтирован');
});
// 2. createEffect — аналог useEffect, запускается при изменении зависимостей
createEffect(() => {
console.log('Реактивное обновление');
});
// 3. onCleanup — аналог componentWillUnmount (или cleanup в useEffect)
onCleanup(() => {
console.log('Компонент удалён из DOM');
});
return <div>Компонент</div>;
}

onMount — это просто createEffect без зависимостей, который запускается после вставки в DOM:

import { onMount, createSignal } from 'solid-js';
function Canvas() {
let canvasRef: HTMLCanvasElement;
const [ctx, setCtx] = createSignal<CanvasRenderingContext2D | null>(null);
onMount(() => {
// DOM гарантированно доступен
const context = canvasRef.getContext('2d')!;
setCtx(context);
// Начальная отрисовка
context.fillStyle = '#2c67d5';
context.fillRect(0, 0, 300, 150);
});
return <canvas ref={canvasRef!} width={300} height={150} />;
}

Важно: onMount синхронен. Если нужна асинхронная загрузка — используй createResource или createEffect с async IIFE.

onCleanup регистрирует функцию очистки, которая запускается когда:

  • Компонент удаляется из DOM
  • Эффект повторно запускается (между запусками)
  • Владелец (owner) уничтожается
import { createEffect, onCleanup, createSignal } from 'solid-js';
function Timer() {
const [seconds, setSeconds] = createSignal(0);
onMount(() => {
// Запускаем таймер при монтировании
const id = setInterval(() => setSeconds(s => s + 1), 1000);
// onCleanup внутри onMount — очищаем при демонтировании
onCleanup(() => clearInterval(id));
});
return <div>Прошло: {seconds()} сек</div>;
}
// onCleanup внутри createEffect — запускается перед каждым повторным эффектом
function Subscription(props: { channel: string }) {
createEffect(() => {
const channel = props.channel; // Зависимость
const ws = new WebSocket(\`wss://api.example.com/\${channel}\`);
onCleanup(() => {
ws.close(); // Закрываем соединение перед переключением канала
});
});
return <div>Слушаем: {props.channel}</div>;
}

createEffect автоматически отслеживает сигналы, к которым обращается:

import { createSignal, createEffect } from 'solid-js';
function SearchBox() {
const [query, setQuery] = createSignal('');
const [results, setResults] = createSignal<string[]>([]);
createEffect(() => {
const q = query(); // Регистрируем зависимость от query
if (q.length < 2) {
setResults([]);
return;
}
// Этот код запустится при каждом изменении query
const abortController = new AbortController();
fetch(\`/api/search?q=\${q}\`, { signal: abortController.signal })
.then(r => r.json())
.then(setResults)
.catch(() => {}); // Игнорируем ошибку отмены
// Очистка — отменяем предыдущий запрос при новом вводе
onCleanup(() => abortController.abort());
});
return (
<div>
<input value={query()} onInput={e => setQuery(e.target.value)} />
<For each={results()}>{r => <div>{r}</div>}</For>
</div>
);
}
1. Функция компонента вызывается (синхронно)
2. JSX рендерится, создаётся дерево DOM
3. onMount колбэки запускаются (bottom-up: дочерние раньше родительских)
4. createEffect запускается (синхронно после mount)
Изменение сигнала →
5. Затронутые эффекты перезапускаются:
a. Запускается onCleanup предыдущего запуска
b. Запускается тело эффекта заново
Компонент удаляется →
6. onCleanup для эффектов
7. onCleanup для onMount

Solid строит дерево «владельцев» — каждый createEffect, createMemo, createRoot создаёт контекст владельца. onCleanup регистрируется у текущего владельца:

import { createRoot, createSignal, onCleanup, getOwner } from 'solid-js';
// createRoot — создать изолированный контекст реактивности
const dispose = createRoot(cleanup => {
const [count, setCount] = createSignal(0);
createEffect(() => {
console.log('count:', count());
onCleanup(() => console.log('эффект очищен'));
});
// cleanup == dispose функция для всего root
return cleanup;
});
// Уничтожить все эффекты и очистить подписки
dispose();
// ❌ НЕ делай эффекты async
createEffect(async () => {
const data = await fetch('/api/data');
// Зависимости, читаемые ПОСЛЕ await, не отслеживаются!
setData(await data.json());
});
// ✅ Читай зависимости ДО async операции
createEffect(() => {
const id = props.id; // ← Зависимость читается синхронно, отслеживается
const controller = new AbortController();
fetch(\`/api/data/\${id}\`, { signal: controller.signal })
.then(r => r.json())
.then(setData);
onCleanup(() => controller.abort());
});
// ✅ Или используй createResource для async данных
const [data] = createResource(() => props.id, fetchData);