useUnit
useUnit — мост между core-моделью и React-компонентом. Он читает сторы как обычные значения и привязывает события и эффекты как вызываемые функции — всё в scope из ближайшего ScopeProvider. Компонент перерендеривается, когда прочитанный им стор меняется в этом scope.
Передайте один юнит, массив или объект — форма результата повторяет форму входа.
Один стор
const count = useUnit(model.count);count — значение стора в активном scope. Компонент перерендерится, когда model.count изменится именно там — и только там, поэтому тот же компонент под другим ScopeProvider покажет значение того scope. Это и позволяет одному дереву компонентов обслуживать много изолированных состояний (на запрос на сервере, на тест, на закешированный экран).
Событие или эффект
События и эффекты возвращаются функциями, уже привязанными к scope, поэтому вызов сразу диспатчит в нужный scope без лишней обвязки.
const incremented = useUnit(model.incremented);
return <button onClick={() => incremented(1)}>{count}</button>;Эффект вызывается так же и возвращает промис. Не вызывайте «голый» юнит из обработчика, не привязав его — у непривязанного вызова нет scope.
Несколько юнитов сразу
Передайте объект, чтобы прочитать группу юнитов за один вызов. Сторы становятся значениями, вызываемые — функциями.
const model = useUnit({
count: counter.count,
incremented: counter.incremented,
reset: counter.reset,
});
return <button onClick={() => model.incremented(1)}>{model.count}</button>;Или массив, когда позиционная привязка читается лучше:
const [count, incremented] = useUnit([counter.count, counter.incremented] as const);Кастомные shape через @@shape
Простую запись юнитов useUnit читает без всякой помощи. Свойство @@shape нужно для случая, когда объект несёт ещё и не-юниты — вспомогательные методы, конфиг, приватные поля, — а привязать хочется только часть. Объект объявляет свои привязываемые юниты через @@shape, и useUnit идёт по этому объявлению, а не перебирает все ключи, поэтому остальное остаётся нетронутым.
Создавайте такой объект один раз — в фабрике модели или в модульной области, но не в рендере, потому что его юниты должны быть стабильны:
import { SHAPE, useUnit } from "@virentia/react";
function createCounter() {
const count = store(0);
const incremented = event<number>();
reaction({ on: incremented, run: (n) => (count.value += n) });
return {
count,
incremented,
isZero: () => count.value === 0, // метод, а не юнит
[SHAPE]: { count, incremented }, // привязываем только юниты
};
}
const counter = createCounter();
// В компоненте: `isZero` не привязывается, только объявленные юниты.
const { count, incremented } = useUnit(counter);Объявление — это обычно сам объект shape. Также принимается форма функции, возвращающей shape, — на случай, когда shape выводится из самого объекта:
const source = {
count,
[SHAPE]() {
return { count: this.count };
},
};Shape вкладываются на любую глубину: членом shape может быть либо простая запись юнитов, либо ещё один источник с @@shape, и useUnit разрешает всё дерево за один вызов.
const dashboard = {
[SHAPE]: {
header: { title: header.title }, // простая вложенная запись
counter: createCounter(), // вложенный источник @@shape
},
};
const { header, counter } = useUnit(dashboard);
// header.title, counter.count, counter.incremented(1)Тот же протокол работает и для поля модели, прочитанного через useModel или component: поле, объявляющее @@shape, попадает во view как свои привязанные юниты, а маркер наружу не утекает. См. Вложенные модели с @@shape.
Эффекты и pending
effect.pending — это стор, поэтому читайте его рядом с эффектом для UI загрузки. Он публикуется сразу — вне транзакции — так что кнопка блокируется в момент старта эффекта.
const { save, pending } = useUnit({
save: saveFx,
pending: saveFx.pending,
});
return (
<button disabled={pending} onClick={() => save(values)}>
Save
</button>
);Так же работают effect.failData (последняя ошибка), effect.inFlight (число выполняющихся вызовов) и другие юниты эффекта.
Чтение среза стора
useUnit(store) перерендеривается при любом изменении стора. Когда компоненту нужна лишь часть большого стора, подпишитесь на производный стор, чтобы перерендер шёл только при изменении этой части.
const fullName = useUnit(userSlice);
// где, объявлено один раз рядом с моделью:
const userSlice = user.map((u) => `${u.first} ${u.last}`);Объявляйте производный стор один раз — рядом с моделью или через useMemo, — чтобы он был стабилен между рендерами. Создание user.map(...) инлайн на каждый рендер делает новый стор и новую подписку каждый раз.
Для значений из нескольких сторов или тяжёлых вычислений используйте computed:
const total = useUnit(cartTotal);
const cartTotal = computed(() => cart.value.items.reduce((sum, i) => sum + i.price, 0));Мутабельные сторы
Мутабельный стор работает с useUnit, как любой стор, — но useUnit(store) читает всё значение, поэтому компонент перерендеривается на каждый commit. Чтобы перерендеривать только при изменении среза, подпишитесь на этот срез через map или computed:
const cart = mutableStore({ items: [] as Item[], coupon: null as string | null });
const couponStore = cart.map((c) => c.coupon);
function Coupon() {
const coupon = useUnit(couponStore);
// Перерендерится только при изменении `coupon` — `cart.value.items.push(...)` не тронет.
return <span>{coupon ?? "нет купона"}</span>;
}Мутабельный draft трекает чтения по keypath, поэтому производный стор подписывается только на части, которые прочитал его селектор. См. Гранулярная реактивность.
useUnit против useModel
useUnit привязывает юниты, которые у вас уже есть. Когда состояние принадлежит самому компоненту — создаётся из пропсов и уничтожается при размонтировании — берите useModel или component: они строят модель и затем разворачивают её так же, как useUnit.