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);Эффекты и 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.