5. Эффекты: createEffect
createEffect: реакции на изменения 🔁
Заголовок раздела «createEffect: реакции на изменения 🔁»Привет! 👋 Яша здесь. Мы уже знаем, как создавать сигналы и читать их значения. Теперь разберём эффекты — механизм, который позволяет реагировать на изменения сигналов и выполнять побочные эффекты: запросы к API, изменение заголовка страницы, аналитика, подписки.
createEffect — это Solid-аналог useEffect, но с принципиально другим механизмом отслеживания зависимостей.
📦 Базовый синтаксис
Заголовок раздела «📦 Базовый синтаксис»import { createSignal, createEffect } from 'solid-js';
const [count, setCount] = createSignal(0);
// Эффект запускается СРАЗУ, затем при каждом изменении зависимостейcreateEffect(() => { console.log('Счётчик изменился:', count()); // count() вызывается внутри — Solid автоматически регистрирует зависимость});
setCount(1); // → «Счётчик изменился: 1»setCount(5); // → «Счётчик изменился: 5»Ключевые отличия от useEffect:
| Аспект | useEffect (React) | createEffect (Solid) |
|---|---|---|
| Зависимости | Явный массив [a, b, c] | Автоматически — всё, что вызвано внутри |
| Первый запуск | После рендера | Сразу при создании |
| Повторный запуск | При изменении deps | При изменении любого вызванного сигнала |
| Cleanup | Возвращаемая функция | Функция onCleanup() |
| Stale closure | Проблема с deps | Не существует — геттер всегда актуален |
🔍 Автоматическое отслеживание зависимостей
Заголовок раздела «🔍 Автоматическое отслеживание зависимостей»Это самая мощная особенность createEffect. Не нужно указывать, от чего зависит эффект — Solid определяет это сам, наблюдая, какие сигналы вызываются при выполнении:
import { createSignal, createEffect } from 'solid-js';
const [firstName, setFirstName] = createSignal('Яша');const [lastName, setLastName] = createSignal('Иванов');const [age, setAge] = createSignal(25);const [showAge, setShowAge] = createSignal(true);
createEffect(() => { // Зависимости: firstName, lastName, showAge — и УСЛОВНО age let info = firstName() + ' ' + lastName();
if (showAge()) { // age() вызывается только если showAge() === true! info += ', ' + age() + ' лет'; }
document.title = info;});
// При setFirstName('Миша') — эффект пересчитывается// При setAge(26) — эффект пересчитывается ТОЛЬКО если showAge() === true// Если showAge() === false, age() не вызывается → нет подписки на age!Это называется динамическое отслеживание зависимостей. Зависимости могут меняться от запуска к запуску в зависимости от условий.
🧹 Функция очистки: onCleanup
Заголовок раздела «🧹 Функция очистки: onCleanup»Для отписки от событий, отмены таймеров и других cleanup-операций используй onCleanup:
import { createSignal, createEffect, onCleanup } from 'solid-js';
function Timer() { const [seconds, setSeconds] = createSignal(0); const [running, setRunning] = createSignal(false);
createEffect(() => { if (!running()) return; // не запускаем таймер, если stopped
// Запускаем интервал const id = setInterval(() => { setSeconds(s => s + 1); }, 1000);
// onCleanup вызывается при: // 1. Повторном запуске эффекта (перед следующим запуском) // 2. Уничтожении компонента onCleanup(() => { clearInterval(id); console.log('Таймер остановлен'); }); });
return ( <div> <p>Секунды: {seconds()}</p> <button onClick={() => setRunning(r => !r)}> {running() ? 'Стоп' : 'Старт'} </button> </div> );}Отмена fetch-запросов
Заголовок раздела «Отмена fetch-запросов»import { createSignal, createEffect, onCleanup } from 'solid-js';
function SearchResults() { const [query, setQuery] = createSignal(''); const [results, setResults] = createSignal([]);
createEffect(() => { const q = query(); if (!q) return;
// AbortController для отмены предыдущего запроса const controller = new AbortController();
fetch('/api/search?q=' + q, { signal: controller.signal }) .then(r => r.json()) .then(data => setResults(data)) .catch(err => { if (err.name !== 'AbortError') console.error(err); });
// При следующем запуске (новый query) — отменяем предыдущий fetch onCleanup(() => controller.abort()); });
return ( <ul> <For each={results()}> {(item) => <li>{item.title}</li>} </For> </ul> );}📊 Порядок выполнения эффектов
Заголовок раздела «📊 Порядок выполнения эффектов»Эффекты выполняются в порядке создания:
createEffect(() => console.log('Эффект 1:', count()));createEffect(() => console.log('Эффект 2:', count()));createEffect(() => console.log('Эффект 3:', count()));
setCount(5);// Вывод:// «Эффект 1: 5»// «Эффект 2: 5»// «Эффект 3: 5»Вложенные эффекты
Заголовок раздела «Вложенные эффекты»createEffect(() => { console.log('Внешний эффект:', count());
// Внутренний эффект — создаётся при каждом запуске внешнего // Это редко нужно, но возможно createEffect(() => { console.log('Внутренний эффект:', name()); });});// ⚠️ Осторожно с вложенными эффектами — легко получить утечку памяти!// Используй onCleanup для очистки вложенных подписок🔇 untrack: отключение отслеживания
Заголовок раздела «🔇 untrack: отключение отслеживания»Иногда нужно прочитать сигнал внутри эффекта, не создавая зависимость:
import { createSignal, createEffect, untrack } from 'solid-js';
const [count, setCount] = createSignal(0);const [name, setName] = createSignal('Яша');
createEffect(() => { // count() создаёт зависимость — эффект перезапустится при изменении count const currentCount = count();
// untrack: читаем name(), но НЕ создаём зависимость на него const currentName = untrack(() => name());
console.log(currentCount, 'от', currentName); // При setName('Миша') — эффект НЕ перезапустится // При setCount(5) — перезапустится});⏰ createEffect vs on
Заголовок раздела «⏰ createEffect vs on»Функция on — помощник для явного указания источников:
import { createEffect, on } from 'solid-js';
// Эффект срабатывает ТОЛЬКО при изменении count (явно)createEffect(on(count, (current, prev) => { console.log('count изменился:', prev, '→', current);}));
// Несколько источников:createEffect(on([count, name], ([c, n]) => { console.log('Изменилось:', c, n);}));
// defer: НЕ запускать при первом рендереcreateEffect(on(count, (c) => { console.log('После первого изменения:', c);}, { defer: true }));🆚 createEffect vs useEffect — углублённое сравнение
Заголовок раздела «🆚 createEffect vs useEffect — углублённое сравнение»React: замкнутые значения и stale closure
Заголовок раздела «React: замкнутые значения и stale closure»// React: классическая ловушкаfunction Counter() { const [count, setCount] = useState(0);
useEffect(() => { const id = setInterval(() => { setCount(count + 1); // ⚠️ count «замкнуто» на значение при создании! // Всегда будет 0 + 1 = 1 }, 1000); return () => clearInterval(id); }, []); // [] — эффект запускается один раз с count=0
// Правильно: setCount(c => c + 1)}// Solid: нет stale closure — count() всегда актуаленfunction Counter() { const [count, setCount] = createSignal(0);
createEffect(() => { const id = setInterval(() => { setCount(c => c + 1); // ✅ работает правильно }, 1000); onCleanup(() => clearInterval(id)); }); // Нет зависимостей — эффект никогда не перезапускается! // onCleanup вызывается при уничтожении компонента}React: пустой массив зависимостей vs Solid: onMount
Заголовок раздела «React: пустой массив зависимостей vs Solid: onMount»// React: [] = запустить один раз при монтированииuseEffect(() => { fetchData();}, []);
// Solid: для разового запуска используй onMountimport { onMount } from 'solid-js';
onMount(() => { fetchData();});// onMount — это просто createEffect с defer + без реактивности🎮 Playground: визуализатор эффектов
Заголовок раздела «🎮 Playground: визуализатор эффектов»Интерактивный граф зависимостей: меняй сигналы и смотри, какие эффекты срабатывают: