Svelte и stores

В Svelte управление состоянием строится вокруг реактивной модели, где изменения данных автоматически отражаются в интерфейсе без необходимости вручную вызывать обновления. Помимо локального состояния компонентов, фреймворк предоставляет отдельный механизм — stores, предназначенный для разделения и масштабирования состояния между компонентами и модулями приложения.

Stores представляют собой объекты с подписочной моделью, реализующие паттерн наблюдателя. Основная идея заключается в том, что состояние хранится вне компонентов, а доступ к нему осуществляется через единый интерфейс подписки.


Базовые принципы store-архитектуры

Store в Svelte — это объект, который реализует минимальный контракт:

  • метод subscribe
  • иногда методы set и update (в случае writable store)
const store = {
  subscribe: (callback) => {
    callback(value);
    return () => {};
  }
};

Любой объект, соответствующий этому интерфейсу, может использоваться как store.

Ключевая особенность: Svelte не требует наследования или специального класса. Достаточно соблюдать контракт подписки.


Writable store: изменяемое состояние

Наиболее часто используемый тип store — writable store, позволяющий изменять значение извне.

Создание осуществляется через функцию writable:

import { writable } from 'svelte/store';

const count = writable(0);

Основные методы writable store

  • set(value) — полная замена значения
  • upd ate(fn) — обновление на основе текущего состояния
  • subscribe(fn) — реактивное чтение значения

Пример:

count.se t(10);

count.update(n => n + 1);

Подписка на изменения

Подписка — фундаментальный механизм работы store.

const unsubscribe = count.subscribe(value => {
  console.log(value);
});

Подписка:

  • вызывается сразу при регистрации
  • вызывается при каждом изменении значения
  • возвращает функцию для отписки
unsubscribe();

Этот механизм позволяет интегрировать store с любыми внешними системами: WebSocket, DOM API, сторонними библиотеками.


Использование store в компонентах Svelte

Svelte предоставляет синтаксический сахар — автоматическую подписку через $.

<script>
  import { count } from './store.js';
</script>

<h1>{$count}</h1>

<button on:click={() => count.update(n => n + 1)}>
  Increment
</button>

Особенности:

  • $count автоматически подписывается на store
  • отписка происходит автоматически при уничтожении компонента
  • доступ к значению выглядит как к локальной переменной

Этот механизм делает store прозрачным внутри UI-логики.


Readable store: только для чтения

Readable store используется, когда состояние не должно изменяться извне.

import { readable } from 'svelte/store';

const time = readable(new Date(), set => {
  const interval = setInterval(() => {
    set(new Date());
  }, 1000);

  return () => clearInterval(interval);
});

Особенности readable store

  • значение устанавливается только внутри фабричной функции
  • внешний код не может вызвать set
  • используется для времени, геолокации, сокетов, событий

Механизм cleanup-функции важен: он освобождает ресурсы при отсутствии подписчиков.


Derived store: вычисляемое состояние

Derived store позволяет строить новое состояние на основе других stores.

import { derived } from 'svelte/store';

const doubled = derived(count, $count => $count * 2);

Несколько зависимостей

const sum = derived(
  [a, b],
  ([$a, $b]) => $a + $b
);

Асинхронный derived store

const user = derived(id, ($id, set) => {
  fetch(`/api/user/${$id}`)
    .then(r => r.json())
    .then(set);

  return () => {};
});

Derived stores:

  • не хранят состояние самостоятельно
  • пересчитываются при изменении зависимостей
  • могут работать асинхронно

Кастомные stores

Любой объект с методом subscribe может быть store. Это позволяет создавать собственные абстракции.

Пример счётчика с инкапсуляцией

function createCounter() {
  const { subscribe, set, upd ate } = writable(0);

  return {
    subscribe,
    increment: () => upd ate(n => n + 1),
    decrement: () => upd ate(n => n - 1),
    reset: () => se t(0)
  };
}

export const counter = createCounter();

Преимущества:

  • скрытие внутренней реализации
  • ограниченный API
  • удобство масштабирования

Store как модуль состояния

Stores в Svelte часто используются как глобальные модули состояния.

Пример структуры:

src/
  stores/
    auth.js
    cart.js
    settings.js

Пример auth store

import { writable } from 'svelte/store';

export const user = writable(null);

export const login = (data) => {
  user.se t(data);
};

export const logout = () => {
  user.se t(null);
};

Такой подход позволяет:

  • отделить бизнес-логику от UI
  • использовать состояние в любом компоненте
  • избегать prop drilling

Реактивность через auto-subscriptions

Svelte компилирует $store в подписку и локальную переменную.

$: doubled = $count * 2;

Store и реактивные выражения часто комбинируются:

  • $store — реактивное значение
  • $: — реактивные вычисления

Поток данных и ограничения

Stores реализуют однонаправленный поток данных:

  1. изменение состояния через set или update
  2. уведомление подписчиков
  3. обновление UI

Запрещается:

  • мутировать значение напрямую без set
  • полагаться на синхронное обновление UI
  • хранить несериализуемые состояния без необходимости

Интеграция store с внешними системами

Stores часто используются как адаптер между Svelte и внешними API.

WebSocket store

import { writable } from 'svelte/store';

export function createSocketStore(url) {
  const { subscribe, set } = writable(null);

  const socket = new WebSocket(url);

  socket.onmess age = (event) => {
    set(JSON.parse(event.data));
  };

  return { subscribe };
}

Производительность stores

Stores в Svelte оптимизированы под минимальные издержки:

  • подписки вызываются только при изменении значения
  • отсутствует виртуальный DOM для состояния
  • обновления батчатся компилятором

Однако возможны проблемы:

  • частые set могут вызывать лишние обновления UI
  • сложные derived stores могут создавать цепочки пересчётов
  • неправильная структура может привести к избыточным подпискам

Store и контекст приложения

Stores часто заменяют глобальный state management, но не всегда являются полной альтернативой сложным решениям вроде Redux.

Сильные стороны:

  • минимализм
  • встроенность в фреймворк
  • прозрачная реактивность

Слабые стороны:

  • отсутствие строгой архитектуры по умолчанию
  • необходимость дисциплины в крупных проектах
  • ограниченные инструменты devtools

Композиция stores

Stores можно комбинировать для построения сложных систем состояния.

const firstName = writable('John');
const lastName = writable('Doe');

const fullName = derived(
  [firstName, lastName],
  ([$firstName, $lastName]) => `${$firstName} ${$lastName}`
);

Композиция:

  • снижает дублирование логики
  • улучшает читаемость состояния
  • позволяет строить декларативные зависимости

Store в архитектуре приложения

В реальных приложениях stores выполняют роль:

  • слоя состояния (state layer)
  • связующего звена между UI и API
  • механизма реактивной синхронизации данных

Типичная архитектура:

  • UI-компоненты используют $store
  • stores инкапсулируют бизнес-логику
  • сервисы выполняют запросы и обновляют stores

Ошибки при работе со stores

На практике часто встречаются проблемы:

  • хранение избыточных данных в одном store
  • отсутствие разделения ответственности
  • прямое мутирование сложных объектов внутри store
  • использование store там, где достаточно локального состояния

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

Локальное состояние:

  • ограничено компонентом
  • проще
  • не требует подписок

Stores:

  • глобальные или разделяемые
  • реактивные вне дерева компонентов
  • подходят для сложной логики

Выбор зависит от области применения данных, а не от их типа.