Ловушки реактивности и как их избегать
Реактивность 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? И наконец: не передаёте ли вы в функцию снимок значения вместо геттера? В девяти случаях из десяти проблема — это потеря связи с реактивным источником в одной из этих точек. Полезно держать в голове простой образ: реактивность — это провод от источника к потребителю, и любой из перечисленных промахов перерезает этот провод. Научившись быстро находить место разрыва, вы перестанете воспринимать реактивность как капризную магию и начнёте видеть в ней предсказуемый механизм с понятными правилами.
Итог: большинство проблем с реактивностью — это потеря связи с реактивным источником через деструктуризацию или снимок значения. Держитесь за источник (объект или геттер), и контракт будет соблюдён.