Руна $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 запускает побочные действия в ответ на изменение зависимостей, отслеживает их автоматически и поддерживает очистку. Используйте его для взаимодействия с внешним миром, а не для вычислений.

Проверьте себя
1. Когда следует использовать $effect вместо $derived?
AКогда нужно вычислить значение из состояния
BКогда нужно выполнить побочное действие: запрос, запись в localStorage, работу с canvas
CКогда нужно объявить пропс
DКогда нужно создать переход
2. Зачем функция, переданная в $effect, может возвращать другую функцию?
AЧтобы вернуть вычисленное значение
BЧтобы задать функцию очистки, вызываемую перед повтором и при размонтировании
CЧтобы зарегистрировать новый маршрут
DЭто запрещено синтаксисом