Руна $effect: побочные эффекты
Руна $effect запускает побочное действие, когда меняются её реактивные зависимости — для всего, что выходит за пределы чистых вычислений.
«Вычисляете — derived. Действуете — effect.» Это золотое правило выбора между двумя рунами.
Не всё, что должно реагировать на изменение состояния, — это вычисление чистого значения. Иногда нужно совершить действие: записать в localStorage, обратиться к серверу, нарисовать что-то на canvas, подписаться на WebSocket. Для таких побочных эффектов есть руна $effect. Она запускает переданную функцию после монтирования компонента и затем повторно — каждый раз, когда меняется любая прочитанная внутри неё реактивная зависимость.
Главное отличие от $derived: $effect ничего не возвращает в реактивный граф — он взаимодействует с внешним миром. И ещё одна важная деталь: эффекты выполняются после того, как компонент отрисован в DOM, и в микрозадаче после изменения состояния. То есть к моменту запуска эффекта DOM уже актуален.
<script>
let count = $state(0);
$effect(() => {
// запускается при монтировании и при каждом изменении count
localStorage.setItem('count', count);
console.log('сохранили', count);
// функция очистки: выполнится перед следующим запуском и при удалении
return () => console.log('очистка перед следующим прогоном');
});
</script>
<button onclick={() => count++}>{count}</button>Зависимости эффект отслеживает автоматически: Svelte смотрит, какие реактивные значения вы прочитали внутри функции, и подписывает эффект на них. Не нужно вручную перечислять массив зависимостей, как в React. Если эффект возвращает функцию, она работает как очистка: вызывается перед каждым повторным запуском и при размонтировании компонента — идеально для отписок и таймеров.
Как это работает под капотом
Смоделируем эффект с автоматическим отслеживанием зависимостей и очисткой. Хитрость в том, что во время выполнения эффекта мы запоминаем, какие реактивные значения он читал.
// Упрощённая модель $effect с авто-зависимостями и очисткой
let activeEffect = null;
function reactive(initial) {
let v = initial; const subs = new Set();
return {
get() { if (activeEffect) subs.add(activeEffect); return v; }, // запомнили читателя
set(n) { v = n; subs.forEach(fn => fn()); }
};
}
function effect(fn) {
let cleanup;
function run() {
if (cleanup) cleanup(); // очистка перед повтором
activeEffect = run;
cleanup = fn(); // fn может вернуть функцию очистки
activeEffect = null;
}
run();
}
const count = reactive(0);
effect(() => {
console.log('эффект: count =', count.get());
return () => console.log(' ...очистка');
});
count.set(1); // очистка, затем эффект: count = 1
count.set(2); // очистка, затем эффект: count = 2Попробуй сам ▶ — вставь код в консоль браузера (F12 → Console) и нажми Enter, чтобы увидеть вывод.
Видно, что перед каждым повторным запуском вызывается очистка — ровно так Svelte освобождает ресурсы между прогонами эффекта.
изменилось состояние
|
v
очистка предыдущего прогона (если была)
|
v
выполнить тело эффекта -> побочное действие (fetch, localStorage, canvas)
|
v
запомнить новую функцию очисткиЧастые ошибки
- Использовать
$effectдля вычисления значения. Это работа$derived; эффекты для действий. - Менять внутри эффекта состояние, которое он же читает. Это создаёт бесконечный цикл.
- Забывать очистку для подписок и таймеров. Без неё будут утечки памяти.
Best practices
- Спрашивайте себя: «это вычисление или действие?». Если действие —
$effect. - Всегда возвращайте функцию очистки для таймеров, слушателей и WebSocket.
- Старайтесь обходиться без эффектов там, где хватает
$derived— их избыток усложняет поток данных.
Когда эффект действительно нужен
Начинающие разработчики часто злоупотребляют эффектами, превращая их в свалку для любой логики. Это антипаттерн: чем больше у вас эффектов, тем труднее проследить, что и когда выполняется, и тем выше риск каскадных запусков и бесконечных циклов. Хорошее правило — относиться к $effect как к мосту между реактивным миром Svelte и нереактивным внешним миром: браузерными API, сторонними библиотеками, сетью, хранилищем. Если действие не пересекает эту границу, а просто вычисляет значение, ему место в $derived, а не в эффекте. Реальные уместные сценарии для эффекта: синхронизация с localStorage, управление жизненным циклом подписки на WebSocket, ручная отрисовка на canvas, установка заголовка документа, интеграция со старым плагином jQuery. Во всех этих случаях не забывайте про функцию очистки — именно она отличает аккуратный эффект от источника утечек памяти.
Итог: $effect запускает побочные действия в ответ на изменение зависимостей, отслеживает их автоматически и поддерживает очистку. Используйте его для взаимодействия с внешним миром, а не для вычислений.