useReducer, разделение состояния и команд, Context, useSyncExternalStore и границы глобального состояния.
Глобальное состояние не является целью: задача — сделать переходы явными и ограничить область подписки.
Вы выберете reducer для сложных переходов, безопасно распространите зависимости через Context и подключите внешний store без tearing.
До начала достаточно state ownership, immutable updates и discriminated unions. В конце должен появиться не конспект, а типизированная корзина с reducer, разделёнными contexts и внешним online-status store.
Reducer получает state и action и возвращает следующий state. Он не отправляет запросы, не читает время и не мутирует вход. Чистота позволяет отдельно проверить переходы и повторить action. Event type — discriminated union с минимальным payload.
Следующий фрагмент показывает минимальный рабочий контракт, а не заготовку для слепого копирования.
type Action =
| { type: 'added'; item: Item }
| { type: 'removed'; id: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'added': return { ...state, items: [...state.items, action.item] };
case 'removed': return { ...state, items: state.items.filter(x => x.id !== action.id) };
}
}Action описывает событие, а reducer — единственное место изменения этой модели.
Рабочая проверка. Проверьте каждый переход без render компонента.
Типичная поломка. Вызывать fetch внутри reducer при action submitted.
Action names вроде itemAdded объясняют произошедшее; setState переносит детали модели к каждому caller. Payload содержит необходимые данные, но не DOM Event. События дают журнал для диагностики и облегчают смену внутренней структуры state.
Рабочая проверка. Замените generic setField доменными actions checkout.
Типичная поломка. Передавать reducer MouseEvent и читать target внутри перехода.
Третий аргумент useReducer задаёт lazy initializer. Props не переинициализируют reducer автоматически. Reset может быть action с исходными данными или новая component identity. Решение зависит от того, является ли новая сущность новым экземпляром или событием текущей workflow.
Рабочая проверка. Реализуйте reset checkout после подтверждённого заказа, не читая изменяемый initial object.
Типичная поломка. Мутировать объект initialState и использовать его для каждого reset.
Context избавляет от передачи prop через промежуточные уровни, но создаёт неявную зависимость consumer от provider. Value должен быть узким, provider — размещён близко к владельцу. Context default не является fallback при provider с undefined; custom hook обычно бросает понятную ошибку без provider.
Рабочая проверка. Создайте useCheckout с явной ошибкой при использовании вне CheckoutProvider.
Типичная поломка. Сложить auth, theme, router, API и весь state приложения в один context object.
Dispatch useReducer имеет стабильную identity. Если state и commands лежат в одном новом object value, каждый state update меняет value для всех consumers. Раздельные contexts позволяют command-only consumer не подписываться на state. Для частых больших обновлений могут понадобиться selectors или внешний store.
Рабочая проверка. Профилируйте кнопку, использующую только dispatch, до и после разделения contexts.
Типичная поломка. Мемоизировать огромный context object, не измерив consumers и частоту изменений.
Selector — чистая функция State -> Value. Он централизует знание структуры и упрощает тесты. Мемоизация нужна только для дорогого вычисления или стабильной ссылки у подписки; selector не должен отправлять запросы. Внешние store-библиотеки могут подписывать по selector, обычный Context — нет автоматически.
Рабочая проверка. Выделите selectors subtotal/discount/total и проверьте граничные случаи.
Типичная поломка. Хранить subtotal и total отдельно от items и обновлять в каждом reducer case.
useSyncExternalStore и snapshot contractВнешний store подключается через subscribe и getSnapshot; для SSR добавляется getServerSnapshot. Snapshot должен быть кэшированным/immutable: пока store не изменился, повторный вызов возвращает значение, равное по Object.is. React координирует чтения, чтобы избежать tearing между concurrent renders.
Следующий фрагмент показывает минимальный рабочий контракт, а не заготовку для слепого копирования.
const online = useSyncExternalStore(
networkStore.subscribe,
networkStore.getSnapshot,
networkStore.getServerSnapshot,
);Не создавайте новый object внутри getSnapshot без кэша: React сочтёт store изменившимся при каждом чтении.
Рабочая проверка. Сделайте online store и проверьте unsubscribe после unmount.
Типичная поломка. getSnapshot: () => ({ online: navigator.onLine }) с новым object всегда.
Начинайте с локального state у владельца. Context нужен для относительно стабильной зависимости по subtree. Внешний store оправдан множеством независимых подписчиков, частыми обновлениями, selectors, данными вне React или межстраничным lifetime. Server cache не надо автоматически копировать в client store.
Рабочая проверка. Классифицируйте state приложения: field draft, theme, auth session, server list, websocket ticker.
Типичная поломка. Помещать каждый input keystroke в глобальный store.
Соберите checkout state machine. Reducer обрабатывает add/remove/setQuantity/coupon/submit outcomes, Context разделяет state и commands, а внешний store сообщает online status через useSyncExternalStore. Добавьте selector tests.
Критерий завершения: reducer чист и исчерпывающ, consumers не перерисовываются от неиспользуемого command context, external snapshot стабилен. Сначала зафиксируйте наблюдаемое поведение тестом или измерением, затем меняйте реализацию.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: Effects, refs и внешние системы