Skip to content

useUnit

useUnit — мост между core-моделью и React-компонентом. Он читает сторы как обычные значения и привязывает события и эффекты как вызываемые функции — всё в scope из ближайшего ScopeProvider. Компонент перерендеривается, когда прочитанный им стор меняется в этом scope.

Передайте один юнит, массив или объект — форма результата повторяет форму входа.

Один стор

tsx
const count = useUnit(model.count);

count — значение стора в активном scope. Компонент перерендерится, когда model.count изменится именно там — и только там, поэтому тот же компонент под другим ScopeProvider покажет значение того scope. Это и позволяет одному дереву компонентов обслуживать много изолированных состояний (на запрос на сервере, на тест, на закешированный экран).

Событие или эффект

События и эффекты возвращаются функциями, уже привязанными к scope, поэтому вызов сразу диспатчит в нужный scope без лишней обвязки.

tsx
const incremented = useUnit(model.incremented);

return <button onClick={() => incremented(1)}>{count}</button>;

Эффект вызывается так же и возвращает промис. Не вызывайте «голый» юнит из обработчика, не привязав его — у непривязанного вызова нет scope.

Несколько юнитов сразу

Передайте объект, чтобы прочитать группу юнитов за один вызов. Сторы становятся значениями, вызываемые — функциями.

tsx
const model = useUnit({
  count: counter.count,
  incremented: counter.incremented,
  reset: counter.reset,
});

return <button onClick={() => model.incremented(1)}>{model.count}</button>;

Или массив, когда позиционная привязка читается лучше:

tsx
const [count, incremented] = useUnit([counter.count, counter.incremented] as const);

Эффекты и pending

effect.pending — это стор, поэтому читайте его рядом с эффектом для UI загрузки. Он публикуется сразу — вне транзакции — так что кнопка блокируется в момент старта эффекта.

tsx
const { save, pending } = useUnit({
  save: saveFx,
  pending: saveFx.pending,
});

return (
  <button disabled={pending} onClick={() => save(values)}>
    Save
  </button>
);

Так же работают effect.failData (последняя ошибка), effect.inFlight (число выполняющихся вызовов) и другие юниты эффекта.

Чтение среза стора

useUnit(store) перерендеривается при любом изменении стора. Когда компоненту нужна лишь часть большого стора, подпишитесь на производный стор, чтобы перерендер шёл только при изменении этой части.

tsx
const fullName = useUnit(userSlice);
// где, объявлено один раз рядом с моделью:
const userSlice = user.map((u) => `${u.first} ${u.last}`);

Объявляйте производный стор один раз — рядом с моделью или через useMemo, — чтобы он был стабилен между рендерами. Создание user.map(...) инлайн на каждый рендер делает новый стор и новую подписку каждый раз.

Для значений из нескольких сторов или тяжёлых вычислений используйте computed:

tsx
const total = useUnit(cartTotal);
const cartTotal = computed(() => cart.value.items.reduce((sum, i) => sum + i.price, 0));

Мутабельные сторы

Мутабельный стор работает с useUnit, как любой стор, — но useUnit(store) читает всё значение, поэтому компонент перерендеривается на каждый commit. Чтобы перерендеривать только при изменении среза, подпишитесь на этот срез через map или computed:

tsx
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.