Svelte

Универсальная реактивность: руны вне компонентов

Корзина видна в шапке, на странице товара и в боковой панели - три компонента, одно состояние. До Svelte 5 ответ был один: завести writable store, подписаться через автоподписку с символом доллара, не забыть про set и update. В Svelte 5 руна `$state` работает не только внутри .svelte, но и в обычном файле .svelte.js. Состояние корзины объявляется один раз в модуле, импортируется в любой компонент и остаётся реактивным везде. Та же руна, что и в компоненте, только теперь общая на всё приложение.

  • Корзина магазина: один модуль cart.svelte.js с массивом товаров, импортируемый в шапку, каталог и оформление заказа
  • Текущий пользователь: модуль auth.svelte.js хранит профиль и флаг входа для всего приложения
  • Тема оформления: светлая или тёмная тема в одном месте, к которому обращаются все компоненты
  • Тосты и уведомления: общий список, в который любой компонент добавляет сообщение
  • Состояние онлайн-плеера: трек, прогресс и громкость, видимые из мини-плеера и полноэкранного режима

Предварительные знания

  • Руна `$state` и её поведение внутри компонента
  • JavaScript-модули: export и import между файлами
  • Понимание того, что объект передаётся по ссылке, а примитив - по значению

От stores к универсальным рунам

До пятой версии общим состоянием в Svelte заведовали stores из svelte/store: writable, readable, derived. Это был отдельный API со своими правилами - подписка, отписка, автоподписка через префикс доллара в шаблоне. Svelte 5 принёс руны и идею универсальной реактивности: один и тот же механизм работает и в компонентах, и в обычных JS-файлах с расширением .svelte.js. Команда Svelte прямо назвала это причиной, по которой stores больше не способ по умолчанию. Stores не удалены и остаются для потоковых сценариев и совместимости, но новое общее состояние пишут на рунах.

Руны в файлах .svelte.js

Расширение .svelte.js говорит компилятору Svelte обработать обычный JavaScript-файл так же, как блок script внутри компонента. Внутри такого файла доступны все руны: `$state`, `$derived`, `$effect`. Разметки тут нет, но реактивность работает: значение, объявленное через `$state`, отслеживается, а зависящий от него `$derived` пересчитывается. Это позволяет держать логику состояния отдельно от любого UI.

Файл объявляет реактивный объект counter и две функции для его изменения. Любой компонент импортирует counter и читает counter.value прямо в разметке - значение остаётся живым. Логика инкремента и сброса живёт рядом с состоянием, а не разбросана по обработчикам в разных компонентах.

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

Почему руну `$state` можно использовать в файле counter.svelte.js, но не в counter.js?

Как экспортировать состояние без потери реактивности

Здесь есть ловушка. Если экспортировать примитив, объявленный через `$state`, реактивная связь теряется. Импорт в JavaScript копирует текущее значение примитива, и компонент получит снимок, а не живую ссылку. Поэтому состояние оборачивают в объект и экспортируют объект, либо отдают доступ через функции-геттеры. Объект передаётся по ссылке, и его реактивный прокси доходит до места импорта целым.

  • Экспорт примитива - связь теряется — export let dark = `$state`(false) с прямым экспортом переменной отдаёт копию значения на момент импорта, и изменения до компонента не доходят
  • Экспорт объекта или геттера - связь жива — Объект передаётся по ссылке, его реактивный прокси сохраняется. Геттер возвращает актуальное значение при каждом чтении

Если нужно отдать именно примитив, используют функцию-геттер: export function getCount() возвращает count при каждом вызове. Тогда реактивность сохраняется, потому что значение читается заново в момент обращения, а не один раз при импорте.

Почему общее состояние из модуля обычно оборачивают в объект, а не экспортируют примитив напрямую?

Конец привычки 'сначала store'

В Svelte 4 общее состояние почти всегда означало store: writable из svelte/store, подписка, автоподписка через префикс доллара в шаблоне. Это работало, но было отдельным API со своими правилами. Универсальная реактивность убирает этот слой. Одна и та же руна описывает и локальное состояние компонента, и общее на всё приложение - менять подход при выносе состояния из компонента больше не нужно.

ЗадачаSvelte 4 (store)Svelte 5 (руны)
Общее значениеwritable(0)`$state` в .svelte.js
Чтение в разметкеПрефикс доллара для автоподпискиОбычное чтение поля
Изменениеset или updateОбычное присваивание
Производное значениеderived из store`$derived` в модуле

Stores не объявлены устаревшими и не удалены. Они остаются хорошим выбором, когда значение приходит из внешнего источника во времени - сокет, поток событий, таймер, - потому что интерфейс store с подпиской ложится на такие сценарии естественно. Но для обычного общего состояния приложения способ по умолчанию в 2026 году - руны в модуле.

Реактивный модуль исполняется один раз на процесс. На сервере при SSR это значит, что состояние модуля разделяется между запросами разных пользователей. Данные конкретного пользователя нельзя держать в состоянии модуля на сервере - для этого есть механизмы запроса в SvelteKit. Модульное состояние безопасно для значений на стороне клиента.

В каком случае store из svelte/store всё ещё уместен в Svelte 5?

Связь с другими темами

Этот урок выносит уже знакомые руны за пределы компонента:

  • `$state`: реактивное состояние — Та же руна, теперь объявляется в модуле .svelte.js
  • `$derived`: производные значения — Производные от общего состояния так же вычисляются в модуле
  • Паттерны состояния: классы и коллекции — Класс с полями `$state` - удобная упаковка вынесенного состояния

Итог

  • Руны работают в файлах .svelte.js и .svelte.ts, а не только в компонентах - это и есть универсальная реактивность
  • Общее состояние объявляется один раз в модуле через `$state` и импортируется в любой компонент
  • Экспортировать нужно объект или геттер, а не голую переменную: при экспорте примитива теряется реактивная связь
  • Импортированное состояние остаётся реактивным во всех компонентах сразу, без подписки и отписки
  • В Svelte 5 руны - способ по умолчанию для общего состояния; stores оставлены для совместимости и потоковых сценариев

Связанные уроки

  • sv-06-state — Универсальная реактивность строится на той же руне `$state`, только теперь вне компонента
  • sv-07-derived — В общих модулях `$derived` так же вычисляет производные от вынесенного состояния
  • sv-11-state-patterns — Классы с `$state`-полями - естественный способ организовать вынесенное состояние из этого урока
Универсальная реактивность: руны вне компонентов

0

1

Войти