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

5. Эффекты: 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!

Это называется динамическое отслеживание зависимостей. Зависимости могут меняться от запуска к запуску в зависимости от условий.


Для отписки от событий, отмены таймеров и других 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>
);
}
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 для очистки вложенных подписок

Иногда нужно прочитать сигнал внутри эффекта, не создавая зависимость:

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) — перезапустится
});

Функция 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: классическая ловушка
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: [] = запустить один раз при монтировании
useEffect(() => {
fetchData();
}, []);
// Solid: для разового запуска используй onMount
import { onMount } from 'solid-js';
onMount(() => {
fetchData();
});
// onMount — это просто createEffect с defer + без реактивности

Интерактивный граф зависимостей: меняй сигналы и смотри, какие эффекты срабатывают: