Ловушки реактивности и как их избегать

Реактивность Svelte мощная, но у неё есть правила: понимание границ убережёт от «почему оно не обновляется».

«Реактивность не магия, а контракт.» Знание этого контракта отличает уверенного разработчика от того, кто борется с фреймворком.

Когда реактивность работает, она кажется волшебством: поменял значение — экран обновился. Но иногда она «не срабатывает», и причина почти всегда в нарушении контракта между вами и компилятором. Разберём самые частые ситуации, чтобы вы их узнавали мгновенно.

Первая ловушка — деструктуризация. Реактивность $state привязана к объекту-прокси. Когда вы пишете const { age } = user, вы достаёте обычное число, которое больше не связано с прокси. Меняя его, вы не тронете состояние. Решение — обращаться через сам объект: user.age, либо делать производное.

Вторая ловушка — потеря реактивности при передаче значения в обычную функцию или модуль. Если вы хотите, чтобы кусок логики оставался реактивным вне компонента, передавайте не само значение, а функцию-геттер или используйте руны в отдельном .svelte.js-файле — такие файлы поддерживают руны.

Третья ловушка касается замыканий: значение, захваченное в момент создания замыкания, может «заморозиться». Покажем это на чистом JS, чтобы интуиция стала прочной.

// Почему 'снимок' значения теряет реактивность
function makeState(v) {
  const box = { value: v };
  return box; // возвращаем коробку, а не само число
}

const state = makeState(10);

// ПЛОХО: сняли снимок — он не обновится
const snapshot = state.value;

// ХОРОШО: читаем через геттер каждый раз
const read = () => state.value;

state.value = 20;
console.log('снимок (заморожен):', snapshot); // 10
console.log('геттер (живой):', read());        // 20

Попробуй сам ▶ — вставь код в консоль браузера (F12 → Console) и нажми Enter, чтобы увидеть вывод.

Снимок остался равным 10, а геттер вернул свежие 20. В Svelte это та же идея: храните доступ к реактивному источнику, а не его мгновенное значение.

Как это работает под капотом

Компилятор Svelte отслеживает зависимости через чтение реактивных значений. Если значение прочитано в реактивном контексте (разметка, $derived, $effect), создаётся подписка. Если же значение «вынуто» наружу обычным присваиванием, контекст теряется, и подписка не образуется.

  $state user = { age: 17 }
     |
     +-- user.age в разметке      -> РЕАКТИВНО (читаем прокси)
     +-- const {age} = user       -> НЕ реактивно (вынули число)
     +-- () => user.age (геттер)   -> РЕАКТИВНО (читаем при вызове)

Частые ошибки

  • Деструктурировать реактивное состояние и удивляться, что интерфейс не обновляется.
  • Передавать значение, а не геттер, в функцию, которая должна реагировать на изменения.
  • Класть руны в обычный .js-файл. Руны вне компонентов работают только в файлах .svelte.js / .svelte.ts.

Best practices

  • Работайте с реактивным объектом через его свойства, а не через снятые копии.
  • Для общей реактивной логики выносите её в .svelte.js-модуль с рунами.
  • Когда что-то «не обновляется», первым делом проверьте, не потеряли ли вы связь с прокси.

Как отлаживать «оно не обновляется»

Когда интерфейс упрямо не реагирует на изменение, выработайте привычку проверять вещи в определённом порядке. Сначала спросите: действительно ли значение объявлено через $state, или это обычное let? Затем: не выдернули ли вы значение из реактивного источника деструктуризацией или присваиванием в локальную переменную? Дальше: если логика вынесена из компонента, лежит ли она в файле .svelte.js, а не в обычном .js? И наконец: не передаёте ли вы в функцию снимок значения вместо геттера? В девяти случаях из десяти проблема — это потеря связи с реактивным источником в одной из этих точек. Полезно держать в голове простой образ: реактивность — это провод от источника к потребителю, и любой из перечисленных промахов перерезает этот провод. Научившись быстро находить место разрыва, вы перестанете воспринимать реактивность как капризную магию и начнёте видеть в ней предсказуемый механизм с понятными правилами.

Итог: большинство проблем с реактивностью — это потеря связи с реактивным источником через деструктуризацию или снимок значения. Держитесь за источник (объект или геттер), и контракт будет соблюдён.

Проверьте себя
1. Где можно использовать руны вне компонента?
AВ любом .js-файле
BТолько в файлах с расширением .svelte.js или .svelte.ts
CТолько внутри функций load
DНигде — руны работают только в .svelte
2. Почему снимок реактивного значения (const x = state.value) теряет реактивность?
AПотому что значение копируется и больше не связано с источником обновлений
BПотому что const нельзя использовать с реактивностью
CПотому что value всегда равно undefined
DПотому что Svelte запрещает чтение состояния