awesome-everything EN
↑ Обратно к восхождению

Архитектура фронтенда

Форма состояния: решение, которое принимаешь до выбора библиотеки

Суть Большинство «багов состояния» — это баги формы: значение, которое хранят, хотя его надо выводить, или серверный кэш, с которым обращаются как с клиентским состоянием. Форма решает радиус перерендеров и какие баги вообще возможны.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 16 min

Выкатили UI фильтров. Пользователи жалуются: бейдж со счётчиком врёт — «127 результатов», а в списке 9. Это не баг рендера: кто-то положил count в useState рядом с массивом results. Один setResults обновился, второй — нет. Два значения, которые должны были быть одним. Бейдж врал три недели, прежде чем кто-то завёл тикет.

Форма — это решение, а не дефолт

Прежде чем тянуться к Redux, Zustand, Jotai или React Query, ты принимаешь решение потише, но важнее: что вообще является состоянием, где оно живёт и оно ли источник истины или копия другого. Угадаешь форму — и большинство «проблем управления состоянием» не появятся. Ошибёшься — и никакая библиотека не спасёт: ты просто размажешь тот же drift по более красивому API.

Форму любого значения решают три вопроса:

  1. Это вообще состояние или производное? Если можно вычислить из существующего состояния прямо в рендере — это не состояние. Хранение создаёт второй источник истины, который рано или поздно разойдётся.
  2. Чьё это состояние — клиента или сервера? Список заказов, полученный из API, — это серверный кэш, а не клиентское состояние. Моделировать его как клиентское — самая частая архитектурная ошибка во фронтенде.
  3. Кто его читает? Этот ответ — один компонент, поддерево или всё приложение — задаёт, где значение живёт и насколько велик радиус перерендеров.

Ловушка производного состояния

Баг с бейджем выше — каноничный провал. count — это results.length. В тот момент, когда ты хранишь count отдельно, ты берёшь на себя инвариант, который фреймворк с радостью держал бесплатно: каждый путь кода, меняющий results, обязан менять и count. Пропусти один путь — обработчик ошибки, оптимистичное обновление, патч по websocket — и значения разойдутся. UI показывает то, что было верным в каком-то прошлом рендере и так и не исправилось.

Рефлекс сеньора: если можно вывести — выводи. Вычисляй в рендере; тянись к useMemo только когда вычисление измеримо дорогое, а не по умолчанию. Мемоизация — инструмент производительности, а не корректности: добавление useMemo не уравнивает «вывести vs хранить», потому что хранимая копия остаётся вторым источником истины даже мемоизированной.

ЗначениеХранить как состояние?Почему
results из APIНет — серверный кэшПринадлежит серверу; нужна свежесть + рефетч, а не useState
count = results.lengthНет — производноеВычислимо в рендере; хранение приглашает drift
isModalOpenДа — локальное UI-состояниеЧисто клиентская забота, читает одно поддерево → колоцируй
selectedIdДа — но храни id, а не объектОбъект выводится из id + кэша; настоящее состояние — это id

Серверный кэш — не клиентское состояние

Самая крупная ошибка формы — загрузить серверные данные в useState/Redux и обращаться с ними так, будто они твои. Они не твои. У серверных данных есть свойства, которых у клиентского состояния не бывает: они устаревают, их можно рефетчить, их нужно дедуплицировать между компонентами, у них есть состояния загрузки и ошибки, их может инвалидировать мутация в другом месте. Когда ты моделируешь их как обычное клиентское состояние, ты вручную пишешь всё это — обычно плохо. Именно поэтому существуют React Query / TanStack Query, SWR и RTK Query: это библиотеки серверного кэша, а не состояния, и в этом различии вся суть.

Практический раздел, который проводит сеньор:

  • Серверный кэш → React Query / SWR / RTK Query. С ключом по запросу, со свежестью, рефетчем, дедупом.
  • Клиентское состояниеuseState (локальное), Context или Zustand/Jotai (общее). То, чего сервер никогда не знал: какая вкладка открыта, черновик формы, выбор.
  • URL → источник истины для всего, что шарится или кладётся в закладки: фильтры, пагинация, id открытой детали. Хранить это только в useState означает: рефреш теряет состояние, а скопированная ссылка мертва.

Колокация задаёт радиус взрыва

Состояние живёт в наименьшем общем предке всех, кто его читает, — и не выше. Поднять значение в глобальный стор «чтобы было доступно» — самая частая причина перерендер-штормов на всё приложение: каждый компонент, подписанный на стор, перерендеривается при изменении, даже те, что не читали изменившийся срез. Мысль Кента Доддса прямая: переместить состояние вниз, туда, где его используют, делает приложение быстрее, потому что React перерендеривает только владеющее поддерево.

Цена реальна на масштабе. Глобальный стор с высокочастотным состоянием (позиция мыши, живой счётчик, нажатия в форме) перерендеривает каждого подписчика на каждое изменение, если не отбирать срезы аккуратно. Atom-библиотеки (Jotai, Recoil) существуют именно чтобы сжать радиус взрыва до компонентов, читающих конкретный атом. Но самый дешёвый фикс обычно структурный: не поднимай то, чем владеет одно поддерево.

Почему это работает

«Поднимай состояние наверх» (фраза из доков React) верна, но её рутинно перегибают. Поднимай к наименьшему общему предку, а не на самый верх. Подъём к корню — это как открыто/закрыто у тултипа начинает перерендеривать дашборд. Зеркальное правило — «опускай состояние вниз» — это и есть колокация на практике.

Нормализация: трейдофф для реляционного клиентского состояния

Когда ты действительно держишь реляционные данные в клиентском сторе (offline-first приложение, редактор с локальными сущностями), формируй их так, как сделала бы база: плоская мапа по id, а не вложенные массивы. { users: { byId: {...}, allIds: [...] } } вместо users, вложенных в posts, вложенных в threads.

Трейдофф конкретен. Вложенная форма делает чтение удобным (post.author.name), но обновление — кошмаром: поменяй имя одного пользователя, и придётся найти и переписать каждую копию, встроенную по всему дереву, — а пропустишь одну, снова drift, теперь на масштабе. Нормализованная форма делает обновление O(1) (запись одной записи в byId) ценой джойна при чтении (byId[post.authorId].name). Для всего, что часто мутируешь, — нормализуй. createEntityAdapter из Redux Toolkit существует, чтобы сделать это дефолтом. Для read-mostly серверных данных не парься — пусть кэш-библиотека держит сырой ответ.

Выбери лучший вариант

Редактор держит на клиенте threads → posts → users, и пользователей постоянно переименовывают и редактируют. Выбери клиентскую форму.

Викторина

Список товаров получают из API и показывают в трёх компонентах. Где он должен жить?

Викторина

У тебя results: Item[] в состоянии, и нужно показать, сколько их. Какой ход сеньора?

Расставь шаги по порядку

Расставь вопросы, которые сеньор задаёт, чтобы решить, где живёт значение:

  1. 1 Выводимо ли из существующего состояния? Если да — не храни, вычисляй в рендере
  2. 2 Если это состояние: оно серверное (полученное) или клиентское (UI/черновик/выбор)?
  3. 3 Серверное → кэш-библиотека (React Query/SWR), с ключом по запросу
  4. 4 Клиентское и шарящееся/в закладки → URL
  5. 5 Клиентское и приватное → наименьший общий предок его читателей (колокация)
Вспомните перед уходом
  1. 01
    Объясни коллеге, почему хранить count рядом с массивом results — это бомба замедленного действия, и что делать вместо этого.
  2. 02
    Почему «серверный кэш vs клиентское состояние» — самый важный вопрос формы, и как он меняет инструментарий?
Итог

Форма состояния решается до любой библиотеки, и она решает, какие баги возможны. Всё выводимое надо выводить в рендере, а не хранить, иначе оно становится вторым источником истины, который разойдётся. Полученные данные — это серверный кэш: отдай их кэш-библиотеке, которая держит свежесть, рефетч, дедуп и инвалидацию, вместо ручного моделирования как клиентского состояния. Шарящееся view-состояние живёт в URL; остальное колоцируется у наименьшего общего предка читателей, что держит радиус перерендеров маленьким. Когда ты по-настоящему держишь реляционные данные на клиенте, нормализуй их в мапу byId, чтобы обновления оставались O(1) и не расходились. Угадай форму — и большинство «проблем управления состоянием» просто не появятся.

Продолжить восхождение ↑Форма состояния: тест с выбором ответа
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.