Skip to content

Сторы

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

ts
const query = store("");
const profile = store({ name: "Ada", age: 36 });

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

ts
const first = scope();
const second = scope();

scoped(first, () => {
  query.value = "docs";
});

scoped(second, () => {
  query.value = "api";
});

В первом scope у query будет "docs", во втором — "api". Код модели при этом один и тот же.

Чтение и запись

Сторы с примитивными значениями читаются через .value.

ts
count.value += 1;

Стор, хранящий объект, всё равно читается через .value. Чтобы обновить его, присвойте новое значение:

ts
profile.value = { ...profile.value, age: profile.value.age + 1 };

Для объектного состояния, которое нужно читать и писать по полям, используйте reactive (см. раздел «Реактивные сторы» ниже).

Глубоко вложенное состояние

Для состояния, которое вы правите на месте, вглубь — документы, модели редакторов, большие таблицы с правкой по ячейкам — см. @virentia/mutable. Его .value — глубоко мутабельный draft (doc.value.blocks[3].items[7].text = "…") с той же реактивностью по keypath: производное значение перезапустится только для прочитанных им частей.

Прямое чтение или запись требуют scope в текущем контексте выполнения. Если код не находится внутри реакции, effect handler или другой ветки, где scope уже известен, откройте его через scoped(scope, fn).

Записи в сторы транзакционные. Несколько записей в одном синхронном дереве вызовов юнитов батчатся в один commit, а реакции видят финальное закоммиченное значение.

ts
reaction({
  on: incremented,
  run() {
    count.value++;
    count.value++;
  },
});

Внутри транзакции следующий код читает текущее draft-значение. Снаружи транзакции подписчики и derived-сторы уведомляются после commit.

Полная модель исполнения описана в разделе Транзакции.

Реактивные сторы

reactive — это стор, поля объекта которого читаются и пишутся напрямую на самом юните, без обёртки .value. Используйте его, когда состояние — это объект, и обновления по полям читаются естественнее, чем замена значения целиком.

ts
const form = reactive({ name: "Ada", age: 36 });

form.age += 1;
form.name = "Grace";

reactive подчиняется тем же правилам, что и store: значение живёт в scope, прямые чтение и запись требуют активного scope, а записи транзакционные.

Удаление поля через delete form.age убирает ключ и уведомляет подписчиков, как любая запись. Поля пишутся присваиванием, поэтому Object.defineProperty на reactive отклоняется. Подписчик с исключением изолируется: он не останавливает остальных подписчиков и реактивное распространение стора.

Используйте readonlyReactive для объекта, который модель показывает наружу, но меняет только через свои реакции или эффекты. Потребители могут читать поля, но не могут им присваивать.

ts
const profile = readonlyReactive({ name: "Ada", age: 36 });

Оба принимают необязательный skip-токен и devtools-опции, как store.

Derived-сторы

Derived-стор нужен, когда значение полностью следует из другого значения. Например, label поиска можно вычислить из query, а не обновлять вручную в каждой реакции.

ts
const queryLabel = query.map((text) => (text ? `Searching: ${text}` : "Search"));

Derived-сторы ленивые. Если на такой стор не подписана реакция или UI, изменение source-стора только помечает его кеш грязным. Пересчет произойдет позже, когда значение явно прочитают. Если derived-стор активен, например на него подписана реакция, изменение зависимостей сразу пересчитает значение и запустит подписчиков только при реальном изменении результата.

Наблюдатель без scope: глобальный, как любой глобальный наблюдатель: он пересчитывает и уведомляет в том scope, где изменился источник — даже в scope, где derived-стор ни разу не читали. Для наблюдения по scope привяжите реакцию через scope: (или сначала прочитайте derived-стор в этом scope).

Если значение зависит от события во времени, это уже не derived-стор. Тогда лучше хранить обычный стор и менять его через реакцию.

Ленивые вычисления

computed нужен для значений, которые выводятся из состояния, но не должны пересчитываться до чтения. Это полезно для тяжелой фильтрации, сортировки, сборки view-model или правил, где зависимости удобнее определить по фактическим чтениям.

ts
const query = store("");
const users = store({ items: [] as User[] });

const visibleUsers = computed(() => {
  const text = query.value.toLowerCase();

  return users.value.items.filter((user) => user.name.toLowerCase().includes(text));
});

visibleUsers ведет себя как read-only стор. Первый read в scope выполнит функцию и запомнит результат. Следующие чтения вернут кеш. Когда query или users изменятся в этом же scope, кеш станет грязным, но значение не пересчитается, пока его снова не прочитают или пока его не наблюдает реакция, подписка или UI.

Обычный store() считается активным всегда: запись должна сохранить новое значение. Derived-сторы активируются только наблюдением. Используйте map, когда нужен простой derived-стор от одного источника. Используйте computed, когда вычисление дорогое, зависит от нескольких сторов или зависит от веток внутри самой функции.