Урок по треку Архитектура фронтенда 7 мин

Архитектура состояния: что, где и чем хранить

Серверное, клиентское, URL- и форменное состояние: где что хранить и чем.

Самая частая архитектурная ошибка во фронтенде: всё состояние в одном глобальном сторе. На собесе ждут, что ты разделишь состояние на виды и для каждого назовёшь подходящее место.

Виды состояния

  1. Серверное: данные, источник правды которых на бэкенде (товары, профиль, заказы). У него есть свои задачи: кеш, устаревание, повторный запрос, дедупликация, инвалидация после мутации. Для него созданы TanStack Query, RTK Query, SWR, Apollo.
  2. Клиентское глобальное: тема, открытая модалка, черновик корзины для гостя, права текущей сессии. Zustand, Redux Toolkit, Jotai, MobX или Context.
  3. Локальное UI-состояние: раскрыт ли аккордеон, значение поиска до отправки. useState в компоненте.
  4. URL-состояние: фильтры, сортировка, страница, выбранная вкладка. Живёт в адресе: им можно поделиться, работает кнопка «назад», переживает перезагрузку.
  5. Состояние формы: значения, ошибки, touched, dirty. React Hook Form, Formik, в React 19 частично useActionState.
  6. Производное: то, что можно вычислить из другого. Его не хранят, его считают (при необходимости с useMemo или селектором).
// Плохо: два источника правды
const [items, setItems] = useState<Item[]>([]);
const [count, setCount] = useState(0); // забудем обновить, и они разойдутся

// Хорошо: производное считаем
const count = items.length;

Почему серверное состояние не кладут в Redux руками

Если грузить данные в useEffect и класть в стор, придётся самому писать: флаги загрузки и ошибки, кеширование, отмену гонок, повторный запрос при фокусе, инвалидацию после мутации. Библиотеки серверного состояния делают это из коробки:

const { data, isPending } = useQuery({
  queryKey: ['products', filters],
  queryFn: () => api.getProducts(filters),
  staleTime: 60_000,
});

const mutation = useMutation({
  mutationFn: api.addToCart,
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['cart'] }),
});

queryKey с фильтрами: при смене фильтров это другой ключ кеша, старый ответ не перетрётся.

Context не стейт-менеджер

Context это способ передать значение вниз без пробрасывания пропсов. Когда значение провайдера меняется, перерисовываются все его потребители, даже если им нужна одна мелочь из объекта. Для редко меняющихся вещей (тема, локаль, текущий пользователь, DI) это идеально. Для часто меняющегося состояния лучше стор с селекторами (Zustand, Redux через useSelector), который перерисует только подписчиков на изменившийся кусок.

Нормализация

Если одна сущность встречается в нескольких местах (пост в ленте и в избранном), храни её один раз по id: { byId: { [id]: Post }, ids: string[] }. Иначе после лайка обновится одна копия, а вторая останется старой. Redux Toolkit даёт для этого createEntityAdapter.

Оптимистичные обновления

Показываем результат сразу, не дожидаясь сервера, а при ошибке откатываем. Подходит для действий с высокой вероятностью успеха: лайк, отметка задачи. В React 19 есть хук useOptimistic, в TanStack Query для этого onMutate с сохранением предыдущего значения и откатом в onError.

Частые ошибки

  • Хранить в сторе то, что можно вычислить.
  • Копировать пропсы в state (useState(props.value)) и удивляться, что значение не обновляется.
  • Фильтры в useState, а не в URL: ссылкой не поделиться, «назад» не работает.
  • Один огромный Context на всё приложение.

Что спросят на собесе

  • Какие виды состояния бывают и где хранить каждый?
  • Зачем TanStack Query, если есть Redux?
  • Почему Context не заменяет стейт-менеджер?
  • Что такое нормализация и когда она нужна?
  • Как сделать оптимистичное обновление и откат?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.