Классические сторы: writable, readable, derived

Классические сторы (writable, readable, derived) — проверенный механизм состояния через подписки, всё ещё полезный в ряде случаев.

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

До рун основным инструментом разделяемого состояния были сторы из модуля svelte/store. Стор — это объект с методом subscribe: вы подписываетесь на изменения и получаете уведомления. Есть три вида: writable (можно читать и писать), readable (только чтение, значение задаётся изнутри) и derived (производный от других сторов).

В компоненте к стору обращаются с префиксом $: $count автоматически подписывается на стор и даёт его текущее значение, отписываясь при размонтировании. Это удобный сахар, который компилятор разворачивает в подписку.

// stores.js
import { writable, derived } from 'svelte/store';

export const count = writable(0);
export const doubled = derived(count, ($count) => $count * 2);
<!-- компонент -->
<script>
  import { count, doubled } from './stores.js';
</script>

<button onclick={() => $count++}>Счёт: {$count}</button>
<p>Удвоено: {$doubled}</p>

Зачем сторы, если есть руны? Сторы дают тонкий контроль над жизненным циклом подписки — это важно для интеграций, где нужно вручную управлять источником значений (например, обёртка над WebSocket или геолокацией через readable со стартовой и финальной функцией). Кроме того, паттерн возврата стора из загрузчика SvelteKit нельзя заменить руной. Команда Svelte подчёркивает: сторы не deprecated.

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

Контракт стора прост: объект с методом subscribe(fn), который немедленно вызывает fn с текущим значением и возвращает функцию отписки. Реализуем writable и derived с нуля — это всего несколько строк.

// Реализация writable и derived с нуля
function writable(initial) {
  let value = initial; const subs = new Set();
  return {
    subscribe(fn) { subs.add(fn); fn(value); return () => subs.delete(fn); },
    set(v) { value = v; subs.forEach(fn => fn(value)); },
    update(fn) { this.set(fn(value)); }
  };
}
function derived(store, fn) {
  return { subscribe(run) { return store.subscribe(v => run(fn(v))); } };
}

const count = writable(0);
const doubled = derived(count, v => v * 2);
doubled.subscribe(v => console.log('doubled =', v)); // doubled = 0
count.set(3);  // doubled = 6
count.update(v => v + 1); // doubled = 8

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

  writable(count)
      |  set / update
      v
  уведомить подписчиков
      |
      +--> derived(doubled) -> пересчёт -> свои подписчики
      +--> $count в компоненте -> обновить разметку

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

  • Забывать отписку при ручной подписке. Префикс $ отписывается сам, а ручной subscribe — нет.
  • Мутировать значение стора без set/update. Подписчики не узнают об изменении.
  • Смешивать руны и сторы хаотично. Для нового кода выбирайте один подход осознанно.

Best practices

  • В новом коде по умолчанию берите руны; сторы — для явного контроля подписок и для возврата из загрузчиков.
  • В компонентах используйте префикс $ — он сам управляет подпиской и отпиской.
  • Для производных значений используйте derived вместо ручной синхронизации сторов.

Две системы реактивности в одном проекте

С появлением рун в проектах нередко соседствуют две системы — руны и сторы, — и это порождает вопрос, как не запутаться. Совет команды Svelte прост: для нового кода по умолчанию берите руны, а сторы применяйте сознательно там, где они дают что-то, чего руны не дают. Таких ситуаций немного, но они реальны. Первая — тонкий контроль над жизненным циклом подписки: readable со стартовой и финальной функциями идеально оборачивает источник вроде геолокации или WebSocket, который нужно завести при первом подписчике и погасить при последнем. Вторая — паттерн возврата стора из загрузчика SvelteKit, который рунами не выразить. Третья — интеграция со сторонними библиотеками, уже говорящими на языке контракта subscribe. Во всех прочих случаях руна короче, быстрее и не требует объяснять новому разработчику, почему в одном репозитории живут две модели реактивности. Осознанный выбор вместо хаотичного смешения — вот что отличает аккуратный проект.

Итог: классические сторы реализуют состояние через контракт subscribe. Руны вытесняют их в большинстве случаев, но для тонкого контроля подписок и возврата из загрузчиков сторы остаются актуальны.

Проверьте себя
1. Что обязан иметь объект, чтобы считаться стором Svelte?
AМетод render()
BМетод subscribe(fn), вызывающий fn с текущим значением
CСвойство $state
DПоле id
2. В каком случае классический стор предпочтительнее рун?
AВсегда — руны устарели
BКогда нужен явный контроль над подписками или возврат стора из загрузчика SvelteKit
CТолько в Svelte 4
DНикогда — сторы deprecated