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`-полями - естественный способ организовать вынесенное состояние из этого урока