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

В современном фронтенде сложилась странная культура. Кажется, что если ты заводишь проект и не подключаешь к нему какой-нибудь тяжеловесный менеджер состояний (Redux, MobX, Zustand или Vuex) в первый же день, то ты «пишешь говнокод» и создаешь «технический долг».
Как человек, который провел сотни часов, разгребая бесконечные цепочки из action $\rightarrow$ reducer $\rightarrow$ selector в проектах, где по сути нужно было просто передать строку из одного компонента в другой, я хочу заявить: большинство приложений не нуждаются в сложном state management.
В этой статье мы разберем, как структурировать данные так, чтобы проект оставался поддерживаемым, но при этом вы не тратили 30% времени на написание бойлерплейта.
Ловушка «Золотого молотка»
Главная проблема начинающих (и не очень) разработчиков — стремление использовать один инструмент для всего. Это называется «эффектом золотого молотка»: когда у тебя в руках молоток, всё вокруг кажется гвоздем.
Типичный сценарий выглядит так:
- Разработчик слышит, что Redux — стандарт индустрии.
- Он внедряет его в проект.
- Теперь, чтобы добавить одно поле «имя пользователя» в профиль, ему нужно создать константу, экшен, модифицировать редюсер и прокинуть селектор в компонент.
- В итоге бизнес-логика размазана по пяти файлам, а кодовая база раздувается.
Прежде чем тянуть в проект библиотеку, задайте себе вопрос: «Какую конкретно проблему я сейчас решаю?». Если ответ «чтобы данные были доступны везде», то вы идете по пути наименьшего сопротивления, который приведет вас в тупик через полгода разработки.
Иерархия состояний: Где хранить данные?
Чтобы не утонуть в сложности, нужно перестать воспринимать «стейт» как единый большой мешок с данными. Состояние бывает разным, и для каждого типа есть свой оптимальный уровень хранения.
1. Локальное состояние (Local State)
Это данные, которые нужны только одному компоненту и его ближайшим потомкам.
-
- Примеры: открыто ли модальное окно, значение в поле ввода, состояние загрузки конкретной кнопки.
-
- Где хранить: Внутри компонента (
useStateв React,refво Vue).
- Где хранить: Внутри компонента (
-
- Правило: Если данные не нужны за пределами этого дерева компонентов — никогда не выносите их в глобальный стор.
2. Состояние ветки/фичи (Feature State)
Это данные, которые разделяют несколько связанных компонентов (например, форма заказа из трех шагов).
-
- Примеры: данные корзины, заполненные поля многошаговой регистрации.
-
- Где хранить: Поднятие состояния (Lifting State Up) до ближайшего общего родителя или использование легковесного контекста (Context API, Provide/Inject).
-
- Правило: Ограничивайте область видимости. Чем меньше компонентов «подписаны» на обновление данных, тем выше производительность и проще отладка.
3. Глобальное состояние (Global State)
Данные, которые действительно нужны почти везде.
-
- Примеры: данные об авторизованном пользователе, тема оформления (темная/светлая), настройки языка, кэш глобальных справочников.
-
- Где хранить: Вот здесь уже можно подключать Zustand, Pinia или специализированные сторы.
-
- Правило: Стор должен быть «тощим». Только самое необходимое.
Стратегия «От простого к сложному»
Вместо того чтобы сразу строить архитектуру «на вырост» (которая в 90% случаев оказывается ошибочной), используйте итеративный подход.
- Начните с пропсов. Да, prop-drilling (передача данных через несколько уровней) раздражает. Но когда вы почувствуете реальную боль от передачи пропса через 5 уровней, вы точно будете знать, какой именно кусок данных нуждается в оптимизации.
- Используйте Composition. Часто проблему prop-drilling можно решить не стором, а композицией компонентов. Передайте дочерний компонент как
children, и вам не придется прокидывать данные через промежуточные слои. - Внедряйте Context/Provide. Когда данных становится много и они нужны в разных частях одной фичи, создайте локальный контекст для этой фичи. Это изолирует логику и не засоряет глобальный стор.
- Идите в State Manager только в конце. Когда вы понимаете, что синхронизация данных между независимыми ветвями дерева стала слишком сложной — только тогда подключайте внешнюю библиотеку.
Как не превратить стор в свалку?
Если вы всё же пришли к использованию глобального менеджера состояний, не допустите типичных ошибок, которые превращают поддержку проекта в ад.
Разделяйте UI-стейт и Серверный стейт
Это самая важная концепция. Огромная часть того, что мы привыкли хранить в Redux/Zustand — это просто кэш ответов от сервера.
-
- UI-стейт: «Открыто ли меню?», «Какой фильтр выбран?». Это меняется мгновенно и живет только в браузере.
-
- Серверный стейт: «Список заказов», «Профиль пользователя». Эти данные приходят с бэкенда, могут устареть, требуют обновления и обработки ошибок.
Решение: Для серверного стейта используйте специализированные инструменты вроде TanStack Query (React Query) или SWR. Они берут на себя кэширование, инвалидацию данных и обработку состояний загрузки/ошибки. В итоге ваш основной стор сократится в 3-4 раза, потому что вам больше не нужны будут экшены типа FETCH_USERS_START, FETCH_USERS_SUCCESS и FETCH_USERS_ERROR.
Практические советы по организации логики
Чтобы код оставался чистым, следуйте этим принципам:
- Принцип единственного источника истины (SSOT). Не дублируйте данные. Если у вас есть список пользователей и текущий выбранный пользователь, храните в сторе только
selectedUserId, а объект пользователя вытягивайте из списка по этому ID. Это избавит вас от ситуаций, когда в одном месте имя пользователя обновилось, а в другом — осталось старым. - Инкапсуляция мутаций. Не меняйте стейт напрямую из компонентов. Создавайте методы-хелперы (actions/mutations) внутри стора. Компонент должен говорить:
store.checkout(), а неstore.setCart([])$\rightarrow$store.setOrder(true)$\rightarrow$store.setNotification('Success'). - Избегайте вычисляемых данных в сторе. Не храните в стейте то, что можно вычислить на лету. Вместо того чтобы хранить
totalPrice, храните массив товаров и создайте геттер/селектор, который посчитает сумму. Это исключает рассинхронизацию данных.
Резюме: Чек-лист выбора инструмента
Если вы сомневаетесь, что выбрать, пройдите по этому списку:
- Данные нужны только в этом компоненте? $\rightarrow$
useState/ref. - Данные нужны в нескольких компонентах одной ветки? $\rightarrow$ Lifting State Up или Context API.
- Данные — это ответ от сервера, который нужно периодически обновлять? $\rightarrow$ React Query / SWR.
- Данные нужны в разных концах приложения и часто меняются? $\rightarrow$ Zustand / Pinia (минималистичные сторы).
- Проект огромный, команда из 20+ человек, строгий аудит каждого изменения состояния? $\rightarrow$ Redux Toolkit / NgRx (строгая структура и предсказуемость).
Итог: Лучший state management — это тот, которого как можно меньше. Чем меньше данных хранится в глобальном состоянии, тем легче тестировать приложение, тем быстрее оно работает и тем проще новому разработчику влиться в проект. Не усложняйте там, где достаточно простого объекта.

