Frontend

Как организовать state management без лишней сложности

Как организовать state management без лишней сложности

В современном фронтенде сложилась странная культура. Кажется, что если ты заводишь проект и не подключаешь к нему какой-нибудь тяжеловесный менеджер состояний (Redux, MobX, Zustand или Vuex) в первый же день, то ты «пишешь говнокод» и создаешь «технический долг».

Как человек, который провел сотни часов, разгребая бесконечные цепочки из action $\rightarrow$ reducer $\rightarrow$ selector в проектах, где по сути нужно было просто передать строку из одного компонента в другой, я хочу заявить: большинство приложений не нуждаются в сложном state management.

В этой статье мы разберем, как структурировать данные так, чтобы проект оставался поддерживаемым, но при этом вы не тратили 30% времени на написание бойлерплейта.

Ловушка «Золотого молотка»

Главная проблема начинающих (и не очень) разработчиков — стремление использовать один инструмент для всего. Это называется «эффектом золотого молотка»: когда у тебя в руках молоток, всё вокруг кажется гвоздем.

Типичный сценарий выглядит так:

  1. Разработчик слышит, что Redux — стандарт индустрии.
  2. Он внедряет его в проект.
  3. Теперь, чтобы добавить одно поле «имя пользователя» в профиль, ему нужно создать константу, экшен, модифицировать редюсер и прокинуть селектор в компонент.
  4. В итоге бизнес-логика размазана по пяти файлам, а кодовая база раздувается.

Прежде чем тянуть в проект библиотеку, задайте себе вопрос: «Какую конкретно проблему я сейчас решаю?». Если ответ «чтобы данные были доступны везде», то вы идете по пути наименьшего сопротивления, который приведет вас в тупик через полгода разработки.

Иерархия состояний: Где хранить данные?

Чтобы не утонуть в сложности, нужно перестать воспринимать «стейт» как единый большой мешок с данными. Состояние бывает разным, и для каждого типа есть свой оптимальный уровень хранения.

1. Локальное состояние (Local State)

Это данные, которые нужны только одному компоненту и его ближайшим потомкам.

    • Примеры: открыто ли модальное окно, значение в поле ввода, состояние загрузки конкретной кнопки.
    • Где хранить: Внутри компонента (useState в React, ref во Vue).
    • Правило: Если данные не нужны за пределами этого дерева компонентов — никогда не выносите их в глобальный стор.

2. Состояние ветки/фичи (Feature State)

Это данные, которые разделяют несколько связанных компонентов (например, форма заказа из трех шагов).

    • Примеры: данные корзины, заполненные поля многошаговой регистрации.
    • Где хранить: Поднятие состояния (Lifting State Up) до ближайшего общего родителя или использование легковесного контекста (Context API, Provide/Inject).
    • Правило: Ограничивайте область видимости. Чем меньше компонентов «подписаны» на обновление данных, тем выше производительность и проще отладка.

3. Глобальное состояние (Global State)

Данные, которые действительно нужны почти везде.

    • Примеры: данные об авторизованном пользователе, тема оформления (темная/светлая), настройки языка, кэш глобальных справочников.
    • Где хранить: Вот здесь уже можно подключать Zustand, Pinia или специализированные сторы.
    • Правило: Стор должен быть «тощим». Только самое необходимое.

Стратегия «От простого к сложному»

Вместо того чтобы сразу строить архитектуру «на вырост» (которая в 90% случаев оказывается ошибочной), используйте итеративный подход.

  1. Начните с пропсов. Да, prop-drilling (передача данных через несколько уровней) раздражает. Но когда вы почувствуете реальную боль от передачи пропса через 5 уровней, вы точно будете знать, какой именно кусок данных нуждается в оптимизации.
  2. Используйте Composition. Часто проблему prop-drilling можно решить не стором, а композицией компонентов. Передайте дочерний компонент как children, и вам не придется прокидывать данные через промежуточные слои.
  3. Внедряйте Context/Provide. Когда данных становится много и они нужны в разных частях одной фичи, создайте локальный контекст для этой фичи. Это изолирует логику и не засоряет глобальный стор.
  4. Идите в State Manager только в конце. Когда вы понимаете, что синхронизация данных между независимыми ветвями дерева стала слишком сложной — только тогда подключайте внешнюю библиотеку.

Как не превратить стор в свалку?

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

Разделяйте UI-стейт и Серверный стейт

Это самая важная концепция. Огромная часть того, что мы привыкли хранить в Redux/Zustand — это просто кэш ответов от сервера.

    • UI-стейт: «Открыто ли меню?», «Какой фильтр выбран?». Это меняется мгновенно и живет только в браузере.
    • Серверный стейт: «Список заказов», «Профиль пользователя». Эти данные приходят с бэкенда, могут устареть, требуют обновления и обработки ошибок.

Решение: Для серверного стейта используйте специализированные инструменты вроде TanStack Query (React Query) или SWR. Они берут на себя кэширование, инвалидацию данных и обработку состояний загрузки/ошибки. В итоге ваш основной стор сократится в 3-4 раза, потому что вам больше не нужны будут экшены типа FETCH_USERS_START, FETCH_USERS_SUCCESS и FETCH_USERS_ERROR.

Практические советы по организации логики

Чтобы код оставался чистым, следуйте этим принципам:

  1. Принцип единственного источника истины (SSOT). Не дублируйте данные. Если у вас есть список пользователей и текущий выбранный пользователь, храните в сторе только selectedUserId, а объект пользователя вытягивайте из списка по этому ID. Это избавит вас от ситуаций, когда в одном месте имя пользователя обновилось, а в другом — осталось старым.
  2. Инкапсуляция мутаций. Не меняйте стейт напрямую из компонентов. Создавайте методы-хелперы (actions/mutations) внутри стора. Компонент должен говорить: store.checkout(), а не store.setCart([]) $\rightarrow$ store.setOrder(true) $\rightarrow$ store.setNotification('Success').
  3. Избегайте вычисляемых данных в сторе. Не храните в стейте то, что можно вычислить на лету. Вместо того чтобы хранить totalPrice, храните массив товаров и создайте геттер/селектор, который посчитает сумму. Это исключает рассинхронизацию данных.

Резюме: Чек-лист выбора инструмента

Если вы сомневаетесь, что выбрать, пройдите по этому списку:

  1. Данные нужны только в этом компоненте? $\rightarrow$ useState / ref.
  2. Данные нужны в нескольких компонентах одной ветки? $\rightarrow$ Lifting State Up или Context API.
  3. Данные — это ответ от сервера, который нужно периодически обновлять? $\rightarrow$ React Query / SWR.
  4. Данные нужны в разных концах приложения и часто меняются? $\rightarrow$ Zustand / Pinia (минималистичные сторы).
  5. Проект огромный, команда из 20+ человек, строгий аудит каждого изменения состояния? $\rightarrow$ Redux Toolkit / NgRx (строгая структура и предсказуемость).

Итог: Лучший state management — это тот, которого как можно меньше. Чем меньше данных хранится в глобальном состоянии, тем легче тестировать приложение, тем быстрее оно работает и тем проще новому разработчику влиться в проект. Не усложняйте там, где достаточно простого объекта.