Руна $state: реактивное состояние

Руна $state делает обычную переменную реактивной: меняете значение — интерфейс сам обновляется.

«Состояние — это просто значение.» В Svelte 5 счётчик — это число, а не объект и не функция. Вы меняете его так же, как любую другую переменную.

Сердце Svelte 5 — руны. Руна — это специальный примитив с префиксом $, который сообщает компилятору о реактивности. Самая важная из них — $state. Она объявляет реактивное состояние: значение, при изменении которого автоматически обновляется всё, что от него зависит — разметка, производные значения, эффекты.

Что особенно ценно в подходе Svelte: нет специального API для работы с состоянием. В React вы пишете const [count, setCount] = useState(0) и вызываете setCount(count + 1). В Svelte 5 вы пишете let count = $state(0) и затем просто count += 1. Переменная остаётся обычной переменной — руна лишь оборачивает её в реактивность под капотом.

До Svelte 5 реактивность была неявной: любое let на верхнем уровне компонента считалось реактивным автоматически. Это было удобно, но имело границы — такая реактивность работала только внутри компонента и не переносилась в обычные JS-файлы. Руны делают реактивность явной и универсальной: $state работает и в компоненте, и в вынесенном модуле.

<script>
  let count = $state(0);
  let user = $state({ name: 'Аня', age: 17 }); // объекты тоже реактивны
</script>

<button onclick={() => count++}>Счёт: {count}</button>
<button onclick={() => user.age++}>Возраст: {user.age}</button>

Важная деталь: $state делает реактивными и вложенные свойства объектов и элементы массивов. Изменение user.age или list.push(x) обновит интерфейс — под капотом Svelte оборачивает объекты в реактивный прокси.

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

Чтобы понять механизм, смоделируем реактивное состояние на чистом JavaScript с помощью прокси. Идея $state в том, что при чтении значения система запоминает, кто его читал, а при записи — уведомляет всех заинтересованных. Вот упрощённая модель.

// Упрощённая модель $state: значение, которое уведомляет подписчиков
function state(initial) {
  let value = initial;
  const subscribers = new Set();
  return {
    get() { return value; },
    set(next) {
      value = next;
      subscribers.forEach(fn => fn(value)); // уведомить всех
    },
    subscribe(fn) { subscribers.add(fn); }
  };
}

const count = state(0);
// 'разметка' подписывается на изменения
count.subscribe(v => console.log('DOM обновился: счёт =', v));

count.set(1); // -> DOM обновился: счёт = 1
count.set(2); // -> DOM обновился: счёт = 2
console.log('текущее значение:', count.get());

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

Настоящая реализация Svelte использует тонкие сигналы и не требует ручной подписки — компилятор расставляет всё за вас. Но суть та же: запись в состояние запускает адресное обновление зависимых частей.

  $state(0)
     |  count++ (запись)
     v
  сигнал помечен 'грязным'
     |
     +--> {count} в разметке   -> обновить текстовый узел
     +--> $derived от count    -> пересчитать
     +--> $effect от count     -> запустить эффект

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

  • Забыть обернуть переменную в $state. Тогда изменение значения не обновит интерфейс.
  • Деструктурировать реактивный объект. const {age} = user даёт обычное число, потерявшее связь с состоянием.
  • Менять состояние внутри $derived. Производные значения должны быть чистыми; для побочных действий есть $effect.

Best practices

  • Храните в состоянии минимально необходимое; всё, что выводится из него, делайте через $derived.
  • Не деструктурируйте реактивные объекты, если хотите сохранить реактивность — обращайтесь через user.age.
  • Для коллекций используйте обычные методы массива (push, splice) — прокси отследит изменения.

Состояние как единственный источник истины

Главная ментальная модель, которую стоит усвоить: интерфейс — это функция от состояния. Вы не «обновляете экран» вручную, как в эпоху jQuery, дописывая текст в узлы по событию. Вместо этого вы описываете, как разметка зависит от состояния, и меняете только состояние — а Svelte сам приводит экран в соответствие. Из этого следует важный принцип: храните в $state минимально необходимый набор данных, от которого можно вывести всё остальное. Если вы ловите себя на том, что вручную синхронизируете две переменные состояния, скорее всего, одна из них должна быть производной. Чем меньше у вас «настоящего» состояния и чем больше вычисляемого, тем меньше шансов получить рассинхрон, когда экран показывает одно, а данные говорят другое. Эта дисциплина окупается с ростом приложения.

Итог: $state превращает переменную в реактивный источник истины. Вы меняете её как обычное значение, а Svelte точечно обновляет всё зависимое — разметку, производные и эффекты.

Проверьте себя
1. Как объявить реактивное состояние в Svelte 5?
Aconst [x, setX] = useState(0)
Blet x = $state(0)
Creactive let x = 0
D$: x = 0
2. Почему деструктуризация реактивного объекта (const {age} = user) ломает реактивность?
AПотому что объекты нельзя деструктурировать в JS
BПотому что вы извлекаете обычное значение, теряющее связь с реактивным прокси
CПотому что $state не работает с объектами
DПотому что age становится строкой