Перейти к содержимому
GitHub

Хранилища

Хранилище — это объект, который позволяет реактивно получать доступ к значению через простой контракт хранилища. Модуль svelte/store содержит минимальные реализации хранилищ, которые соответствуют этому контракту.

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

Присвоения переменным с префиксом $ требуют, чтобы переменная была записываемым хранилищем (writable store), и приведут к вызову метода .set хранилища.

Обратите внимание, что хранилище должно быть объявлено на верхнем уровне компонента — не внутри блока if или функции, например.

Локальные переменные (которые не представляют значения хранилища) не должны иметь префикс $.

<script>
  import { writable } from 'svelte/store';

  const count = writable(0);
  console.log($count); // выведет 0

  count.set(1);
  console.log($count); // выведет 1

  $count = 2;
  console.log($count); // выведет 2
</script>

Когда использовать хранилища

До Svelte 5 хранилища были основным решением для создания реактивных состояний, общих для нескольких компонентов, или для вынесения логики. С появлением рун эти случаи применения значительно сократились.

  • при вынесении логики лучше использовать универсальную реактивность рун: вы можете использовать руны вне верхнего уровня компонентов и даже размещать их в JavaScript или TypeScript файлах (используя расширение .svelte.js или .svelte.ts);
  • при создании общего состояния вы можете создать объект $state, содержащий необходимые значения, а затем управлять этим состоянием.
// state.svelte.js
export const userState = $state({
  name: 'name',
  /* ... */
});
// App.svelte
<script>
  import { userState } from './state.svelte.js';
</script>

<p>Имя пользователя: {userState.name}</p>
<button onclick={() => {
  userState.name = 'новое имя';
}}>
  изменить имя
</button>

Хранилища по-прежнему являются хорошим решением, если у вас сложные асинхронные потоки данных или важно иметь более ручное управление обновлением значений или отслеживанием изменений. Если вы знакомы с RxJs и хотите повторно использовать эти знания, символ $ также вам пригодится.

svelte/store

Модуль svelte/store содержит минимальную реализацию хранилища, которая соответствует контракту хранилища. Он предоставляет методы для создания хранилищ, которые можно обновлять извне, хранилищ, которые можно обновлять только изнутри, а также для комбинирования и создания производных хранилищ.

writable

Функция, которая создает хранилище, значения которого можно задавать «извне» компонентов. Оно создается как объект с дополнительными методами set и update.

  • set — это метод, принимающий один аргумент — значение для установки в хранилище. Значение обновляется, если оно отличается от текущего значения хранилища.
  • update — это метод, который принимает один аргумент — колбэк-функцию. Колбэк принимает текущее значение хранилища в качестве аргумента и возвращает новое значение, которое будет установлено в хранилище.
// store.js
import { writable } from 'svelte/store';

const count = writable(0);

count.subscribe((value) => {
  console.log(value);
}); // выведет '0'

count.set(1); // выведет '1'

count.update((n) => n + 1); // выведет '2'

Если функция передается в качестве второго аргумента, она будет вызвана, когда количество подписчиков изменится с нуля на один (но не с одного на два и т. д.). Этой функции будут переданы:

  • функция set, которая изменяет значение хранилища,
  • и функция update, работающая аналогично методу update хранилища, принимающая колбэк для вычисления нового значения хранилища на основе старого.

Функция должна вернуть метод stop, который будет вызван, когда количество подписчиков изменится с одного на ноль.

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

const count = writable(0, () => {
  console.log('получил подписчика');
  return () => console.log('подписчиков больше нет');
});

count.set(1); // ничего не произойдёт

const unsubscribe = count.subscribe((value) => {
  console.log(value);
}); // выведет 'получил подписчика', затем '1'

unsubscribe(); // выведет 'подписчиков больше нет'

Обратите внимание, что значение writable хранилища теряется при его уничтожении, например, при обновлении страницы. Однако вы можете реализовать собственную логику для синхронизации значения, например, с localStorage.

readable

Создает хранилище, значение которого нельзя задать «извне». Первый аргумент — начальное значение хранилища, а второй аргумент для readable совпадает со вторым аргументом для writable.

import { readable } from 'svelte/store';

const time = readable(new Date(), (set) => {
  set(new Date());

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

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

const ticktock = readable('тик', (set, update) => {
  const interval = setInterval(() => {
    update((sound) => (sound === 'тик' ? 'ток' : 'тик'));
  }, 1000);

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

derived

Создает производное хранилище на основе одного или нескольких других хранилищ. Колбэк выполняется при первой подписке, а затем каждый раз, когда изменяются зависимости хранилища.

В простейшем случае derived принимает одно хранилище, а колбэк возвращает производное значение.

import { derived } from 'svelte/store';

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

Колбэк может задавать значение асинхронно, принимая второй аргумент — set, и, опционально, третий аргумент — update, вызывая их при необходимости.

В этом случае вы также можете передать третий аргумент в derived — начальное значение производного хранилища до первого вызова set или update. Если начальное значение не указано, им будет undefined.

import { derived } from 'svelte/store';

const delayed = derived(
  a,
  ($a, set) => {
    setTimeout(() => set($a), 1000);
  },
  2000
);

const delayedIncrement = derived(a, ($a, set, update) => {
  set($a);
  setTimeout(() => update((x) => x + 1), 1000);
  // Каждый раз, когда $a выдает значение, это порождает два значения:
  // $a сразу и затем $a + 1 через секунду
});

Если вы вернете функцию из колбэка, она будет вызвана в двух случаях:

  • когда колбэк выполнится снова, или
  • когда последний подписчик отпишется.
import { derived } from 'svelte/store';

const tick = derived(
  frequency,
  ($frequency, set) => {
    const interval = setInterval(() => {
      set(Date.now());
    }, 1000 / $frequency);

    return () => {
      clearInterval(interval);
    };
  },
  2000
);

В обоих случаях массив аргументов может быть передан в качестве первого аргумента вместо одного хранилища.

import { derived } from 'svelte/store';

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

const delayed = derived([a, b], ([$a, $b], set) => {
  setTimeout(() => set($a + $b), 1000);
});

readonly

Эта простая вспомогательная функция делает хранилище доступным только для чтения. Вы по-прежнему можете подписываться на изменения исходного хранилища, используя это новое хранилище.

import { readonly, writable } from 'svelte/store';

const writableStore = writable(1);
const readableStore = readonly(writableStore);

readableStore.subscribe(console.log);

writableStore.set(2); // консоль: 2
readableStore.set(2); // ОШИБКА

get

Обычно рекомендуется читать значение хранилища, подписавшись на него и используя значение по мере его изменения. Однако иногда может потребоваться получить значение хранилища, на которое вы не подписаны. get позволяет это сделать.

import { get } from 'svelte/store';

const value = get(store);

Контракт хранилища

store = { subscribe: (subscription: (value: any) => void) => (() => void), set?: (value: any) => void }

Вы можете создавать собственные хранилища, не полагаясь на svelte/store, реализовав контракт хранилища:

  1. Хранилище должно содержать метод .subscribe, который принимает в качестве аргумента функцию подписки. Эта функция подписки должна быть немедленно и синхронно вызвана с текущим значением хранилища при вызове .subscribe. Все активные функции подписки хранилища должны синхронно вызываться при каждом изменении значения хранилища.
  2. Метод .subscribe должен возвращать функцию отписки. Вызов функции отписки должен прекратить подписку, и соответствующая функция подписки больше не должна вызываться хранилищем.
  3. Хранилище может опционально содержать метод .set, который принимает новое значение для хранилища в качестве аргумента и синхронно вызывает все активные функции подписки. Такое хранилище называется записываемым хранилищем.

Для совместимости с RxJS Observables метод .subscribe также может возвращать объект с методом .unsubscribe, вместо того чтобы возвращать функцию отписки напрямую. Однако обратите внимание, что если .subscribe не вызывает функцию подписки синхронно (что не требуется спецификацией Observable), Svelte будет видеть значение хранилища как undefined, пока это не произойдет.