Управление состоянием в React для команд: что и когда использовать
Управление состоянием в React становится проще, когда команды разделяют локальное UI-состояние, серверные данные и общие клиентские хранилища. Узнайте практичный способ выбора.

Почему это быстро становится запутанным
Состояние часто начинается с малого. Один фильтр живет в одном компоненте, модальное окно открывается по одной кнопке, выбранная строка лежит рядом с таблицей, которая ее использует. Пока все работает, ничего не кажется сломанным, и самый простой шаг — передать эти данные на уровень или два ниже.
Потом продукт меняется обычным, скучным образом. В заголовке теперь нужен тот же счетчик фильтров. Сайдбару нужно сбросить таблицу. Диалогу нужно обновить страницу после сохранения. Появляется еще одна функция, и то же значение проходит через четыре компонента, которым оно не нужно, кроме как чтобы передать его дальше.
Именно поэтому прокидывание props в React сначала кажется нормальным, а потом — нет. Первые несколько props легко терпеть. Проблемы начинаются, когда родительский компонент становится диспетчером для состояния, которым сам не пользуется.
Команды обычно замечают боль в мелочах. Кто-то переименовал prop и сломал три экрана. Новый коллега не может понять, где находится настоящий источник истины. Простое изменение занимает лишний час, потому что никто не хочет трогать дерево компонентов.
Тут и начинается спор о хранилищах. Оставить локальное состояние, добавить общее клиентское хранилище или перенести все в один глобальный подход? Такие споры стоят дороже, чем кажется, потому что возникают во время работы над функциями, а не в спокойное время на планировании. Полдня обсуждений инструментов легко сдвигают исправление бага, релиз или небольшой продуктовый эксперимент.
Популярность библиотек только усугубляет ситуацию. Команды часто копируют то, что в моде в соцсетях, или то, что использовали в прошлом проекте. Но это не то же самое, что решать реальную проблему.
Настоящая боль обычно проще. Один тип данных принадлежит серверу. Другой — одному экрану. Третий нужно разделять между удаленными частями приложения. Если команда считает все три одним и тем же, управление состоянием в React превращается в спор о стиле, а не в вопрос проектирования.
Большинству команд не нужна более модная библиотека. Им нужны более четкие правила, где должен жить каждый тип данных.
Что локальное состояние по-прежнему делает лучше всего
Локальное состояние по-прежнему остается самым чистым выбором для UI, который живет и умирает внутри небольшой части страницы. Текстовое поле, чекбокс, открытое или закрытое модальное окно, выбранный пункт в выпадающем списке, шаг мастера — все это должно быть рядом с компонентом, который этим пользуется.
Это важно, потому что код остается понятным. Когда состояние находится рядом с кнопкой или полем формы, которое его меняет, не нужно искать обновления по половине дерева только ради ответа на вопрос, почему открылось модальное окно.
Экран настроек — хороший пример. Если одна форма управляет полем «name», состоянием кнопки «save» и небольшим сообщением об ошибке, локального состояния достаточно. Весь поток понятен в одном месте, а значит, обычно и багов меньше.
Поднимать состояние выше по дереву тоже имеет смысл, но только когда соседние компоненты действительно делят его между собой. Если двум дочерним компонентам нужен один и тот же активный таб или значение фильтра, поднимите это состояние к их родителю и передайте вниз. Если поднимать его слишком рано, именно тогда прокидывание props в React начинает раздражать.
Context тоже бывает полезен, но в узких случаях. Тема оформления, текущая вкладка, состояние открытого сайдбара, выбор языка или простой флаг авторизации хорошо подходят для context, когда многим потомкам нужно читать одно и то же UI-настройку. Держите его небольшим и стабильным. Context становится неудобным, когда в нем живут часто меняющиеся данные или сложная логика обновления.
Локальное состояние начинает расползаться, когда вы видите одни и те же данные в нескольких местах или когда на одно обновление должны реагировать пять компонентов. Оно также плохо подходит, когда данные должны синхронизироваться с сервером, переживать переходы между страницами или поддерживать кэширование и повторную загрузку.
Команды часто усложняют управление состоянием в React, потому что тянутся к хранилищу раньше, чем оно действительно нужно. Локальное состояние скучное, и это часто хороший знак. Если состояние важно только для одной функции и одного экрана, оставьте его там, пока боль не станет настоящей, а не воображаемой.
Рассматривайте данные сервера как отдельную категорию
Много путаницы в управлении состоянием в React начинается тогда, когда команды обращаются с API-данными как с обычным состоянием компонента. Это не одно и то же. Компонент может читать эти данные и показывать их, но он не управляет источником истины.
Серверные данные могут меняться вне дерева React. Другой пользователь может обновить запись. Фоновая задача может изменить статус. Сервер может вернуть новые данные, когда тот же экран откроют снова через пять минут. Поэтому список пользователей, заказов или комментариев ведет себя совсем иначе, чем переключатель в диалоге или поле формы.
У серверных данных есть несколько задач:
- получать данные, не блокируя весь экран
- кэшировать результаты, чтобы интерфейс не мигал при каждом посещении
- повторно запрашивать данные, когда они могут устареть
- отслеживать состояния загрузки и ошибок
- решать, когда кэшированные данные уже устарели
«Устаревшие» звучит драматичнее, чем есть на самом деле. Обычно это значит: «Эта копия, скорее всего, еще подходит, но скоро стоит проверить сервер». Например, список проектов пятнадцатисекундной давности можно показать сразу, а потом тихо обновить в фоне.
Именно здесь хорошо подходят TanStack Query и SWR. Они созданы для серверных данных, поэтому берут на себя кэширование, повторные запросы, устранение дублей запросов, повторные попытки и состояния загрузки без лишнего ручного кода. Если пользователь переключится на другую вкладку и вернется обратно, эти инструменты могут обновить данные автоматически. Если два компонента запросят один и тот же endpoint, они смогут разделить один кэшированный результат вместо двух отдельных запросов.
Команды добавляют себе лишнюю работу, когда получают данные, а потом копируют их в общее клиентское хранилище вроде Redux или Zustand «просто чтобы все лежало в одном месте». В итоге у вас два источника истины: кэш сервера и клиентское хранилище. Их нужно синхронизировать. После каждого создания, обновления или удаления приходится обновлять сервер, обновлять или исправлять кэш, обновлять хранилище и следить, чтобы состояния загрузки и ошибок совпадали. Это быстро надоедает.
Клиентское хранилище все еще имеет смысл для UI-состояния вокруг серверных данных. Текст фильтра, выбранные строки, открытые панели и несохраненные черновики принадлежат клиенту. Сам список, полученный с сервера, обычно — нет. Держите серверные данные в инструменте для серверного состояния, а клиентское состояние — для всего, чем по-настоящему владеет браузер.
Когда общее клиентское хранилище действительно нужно
Общее хранилище полезно, когда одно и то же состояние находится в удаленных друг от друга частях приложения и менять его нужно в нескольких местах. Если один компонент передает данные всего на два уровня вниз, props по-прежнему подходят. Если заголовок, сайдбар, модальное окно и панель настроек читают и обновляют одно и то же значение, props начинают ощущаться как сантехническая работа.
Хорошие примеры легко заметить. Активное рабочее пространство часто влияет одновременно на навигацию, права доступа и основной контент. Межстраничные фильтры могут влиять на таблицы, графики и сохраненные представления. Еще один частый случай — прогресс мастера, особенно когда пользователь возвращается назад, а разные шаги редактируют один и тот же черновик.
Общее клиентское хранилище лучше всего работает и для UI-состояния, которое принадлежит сессии пользователя, а не бэкенду. Если данные в основном приходят из API и вам важны кэширование, повторные запросы и устаревание данных, держите это в инструменте для серверного состояния. В хранилище помещайте небольшой слой общих клиентских данных, а не весь мир.
Zustand и Jotai популярны потому, что требуют совсем немного. Вы можете добавить хранилище за минуты, читать из него там, где нужно, и двигаться дальше. Такая скорость удобна для небольших команд или продуктов, которые меняются каждую неделю.
Redux Toolkit требует больше структуры, но именно эта структура может уберечь команду от неряшливых привычек в будущем. Действия, слайсы и предсказуемые обновления упрощают отслеживание изменений, код-ревью и онбординг новых людей. Цена — более сложная настройка и немного больше формальностей.
Рабочее правило такое:
- Выбирайте Zustand или Jotai, когда общее состояние небольшое, понятное и, скорее всего, скоро изменит форму.
- Выбирайте Redux Toolkit, когда код трогают многие люди, правила строгие или изменения состояния нужно легко отследить.
- Пропускайте общее хранилище, если данные использует только одна ветка дерева.
Большинству команд не нужно большое хранилище на старте. Им нужно небольшое хранилище с четкими границами. Если ваше хранилище начинает собирать в себе ответы API, черновики форм, флаги модальных окон и логику функций, проблема уже не в прокидывание props в React. Проблема в том, что хранилище превратилось в ящик для всего подряд.
Выбирайте по типу данных, а не по привычке команды
Команды застревают, когда сначала выбирают один подход, а потом заставляют каждый тип данных в него помещаться. Так простое управление состоянием в React превращается в споры о хранилищах, селекторах и лишнем коде.
Лучшее правило скучное, и именно поэтому оно работает: сортируйте состояние по тому, откуда оно приходит и кто может его менять. После этого нужный инструмент обычно становится очевиден.
Если значение живет внутри одного экрана и пользователь меняет его там, оставьте его локальным. Поля формы, открытые панели, выбранные вкладки и временные сообщения об ошибке хорошо подходят для состояния компонента или reducer.
Если данными владеет сервер, относитесь к ним как к серверному состоянию. Список пользователей, тарифный план, уведомления или данные отчета должны подчиняться правилам загрузки, кэширования, повторного запроса, загрузки и устаревания. Перенос этого в общее клиентское хранилище часто создает лишнюю работу и баги с устаревшими данными.
Если нескольким удаленным частям приложения нужно читать и обновлять одно и то же клиентское значение, общее хранилище может помочь. Выбор темы, состояние сайдбара, черновики настроек или многошаговый процесс, который растянут на несколько маршрутов, — типичные примеры.
Вот быстрый тест, который обычно все расставляет по местам:
- Если значение использует один экран и меняет тоже один экран: оставьте его локальным.
- Если данными владеет API: используйте инструмент для серверного состояния.
- Если многие части приложения читают и записывают значение на клиенте: используйте общее хранилище.
- Если вы не можете объяснить, кто владеет этим состоянием: остановитесь и сначала определите это.
Одно приложение может использовать все три подхода одновременно. Это нормально, а не запутанно. Страница поиска может держать поле ввода локально, получать результаты через серверный кэш и хранить текущее рабочее пространство в общем клиентском хранилище.
Худшее правило — «одно хранилище для всего». Оно звучит аккуратно, но смешивает временные UI-значения, кэшированные данные API и общие клиентские данные приложения в одну корзину. Из-за этого сложнее понимать изменения. Разделите состояние по владению, и большинство споров о хранилищах исчезнет само собой.
Простой пример с продуктовой панелью
Представьте продуктовую панель с тремя движущимися частями: панелью фильтров сверху, таблицей аккаунтов посередине и выезжающим редактором, который открывается, когда кто-то нажимает на строку. Это как раз тот экран, где прокидывание props в React начинает мешать, потому что нескольким частям страницы нужны одни и те же данные, но не все подряд.
Состояние панели должно остаться локальным. Если пользователь открыл панель, переключил вкладку или начал писать незавершенную заметку в форму, это состояние принадлежит панели и ее дочерним элементам. Никому больше на странице не нужен каждый набираемый символ. Локальное хранение делает код спокойнее и не позволяет общему хранилищу превратиться в ящик для всего подряд.
Таблица — другой случай. Строки, общее количество, пагинация, сортировка, загрузка и повторный запрос относятся к серверному состоянию, потому что этими данными владеет сервер. Когда кто-то переходит со страницы 1 на страницу 2 или меняет фильтр, вам нужны кэшированные результаты и новый запрос при необходимости. Кэш серверного состояния справляется с этим намного лучше, чем обычное клиентское хранилище.
Небольшое общее клиентское хранилище тоже на своем месте. Поместите туда выбранное рабочее пространство, а также фильтры, которые нужны нескольким частям экрана. Заголовок сможет их читать, таблица — использовать в запросах, а график или кнопка экспорта будут оставаться в синхронизации без передачи props через четыре уровня.
Хорошее разделение выглядит так:
- Локальное состояние хранит видимость панели, активную вкладку панели и несохраненные черновики формы.
- Серверное состояние хранит строки таблицы, счетчики, пагинацию, сортировку и статус запроса.
- Общее клиентское хранилище хранит выбранное рабочее пространство и общие фильтры вроде статуса или диапазона дат.
Такое разделение решает две очень частые ошибки команд. Первая — складывать каждую мелочь в глобальное хранилище, потому что так кажется безопаснее. Вторая — обращаться с данными, пришедшими с API, как с обычным клиентским состоянием, а потом через неделю бороться с устаревшими данными и багами перезагрузки.
Допустим, руководитель отдела продаж переключает рабочее пространство с «EU» на «US». Общее хранилище обновляется один раз. Затем таблица запрашивает правильные строки для этого рабочего пространства. Если руководитель открывает запись, редактирует черновую заметку и закрывает панель, этот незавершенный текст может остаться локальным или исчезнуть, не затрагивая остальную панель. Обычно это и есть самый простой вариант управления состоянием в React, и он хорошо работает по мере роста экрана.
Как выбирать шаг за шагом
Команды обычно усложняют это сильнее, чем нужно. Хорошее управление состоянием в React начинается с одного реального экрана, а не с долгого спора об архитектуре.
Откройте страницу, которой люди пользуются часто, и выпишите каждый элемент данных, который меняется. Включите мелочи вроде флага модального окна или поля поиска, а также более крупные вещи вроде загруженных отчетов, данных текущего пользователя или несохраненных правок.
Затем отсортируйте каждый элемент по типу.
- Локальное состояние живет рядом с одним компонентом или небольшой связкой родитель–дочерний компонент. Текст ввода, открытые панели и выбранные вкладки обычно остаются здесь.
- Серверное состояние приходит из API и может требовать повторной загрузки, кэширования или синхронизации после сохранения. Списки, данные аккаунта и числа на панели относятся сюда.
- Общее клиентское состояние живет в браузере и нужно сразу нескольким частям приложения. Тема, статус авторизации, количество товаров в корзине и черновые данные между страницами — частые примеры.
Дальше посчитайте, сколько мест читают каждое значение и сколько мест его меняют. Этот простой шаг многое проясняет. Если один компонент записывает значение, а два соседних его читают, возможно, достаточно просто поднять состояние выше. Если несколько удаленных частей приложения его читают и меняют, общее хранилище начинает иметь смысл.
Выбирайте самое маленькое решение, которое подходит под текущую задачу. Для локального состояния обычно достаточно useState или useReducer. Для серверных данных используйте инструмент для серверного состояния вместо того, чтобы складывать API-данные в клиентское хранилище. Для общего клиентского состояния выбирайте небольшое хранилище только после того, как повторяющаяся боль с синхронизацией действительно проявится в работе.
Я бы не советовал рано добавлять глобальное хранилище. Команды часто делают это, чтобы избежать прокидывания props в React, а потом месяцами разбираются с устаревшими данными, дубликатами кэша и размытым владением.
После выпуска одной функции пересмотрите выбор. Код остался легко изменяемым? Не появились ли два источника истины? Начали ли разработчики копировать данные сервера в клиентское состояние только ради удобства доступа? Один короткий разбор после релиза расскажет больше, чем неделя теорий.
Ошибки, из-за которых команды застревают
Большая часть проблем в командах начинается не с React. Она начинается тогда, когда все типы данных складывают в одну корзину. Поле формы, кэшированный ответ API и переключатель сайдбара не нуждаются в одном и том же инструменте.
Одна из распространенных ошибок — по привычке переносить все полученные с сервера данные в Redux, Zustand или другое общее хранилище. Это добавляет дублирование состояния, лишний код преобразования и тонкие баги, когда данные API изменились, а хранилище — нет. Если данные приходят с сервера, пусть кэшированием, повторной загрузкой и состояниями загрузки занимается инструмент для серверного состояния. Переносите их в клиентское состояние только тогда, когда пользователю нужно отредактировать их локально до сохранения или когда у приложения есть реальные клиентские правила вокруг этих данных.
Context создает другую проблему. Он кажется простым, поэтому команды используют его для часто меняющихся общих данных приложения. Потом поисковая строка, живые фильтры или состояние перетаскивания начинают обновлять полдерева, и приложение становится тяжелее, чем должно. Context хорош для стабильных значений вроде темы, авторизации или языка. Он плохо подходит для состояния, которое меняется на каждый ввод символа или много раз в минуту.
Еще одна ловушка — добавлять хранилище до того, как кто-то сможет назвать второй реальный сценарий использования. Одна неудобная цепочка передачи props может слишком рано подтолкнуть команду к выбору библиотеки. Часто более простое решение — чуть выше поднять состояние, изменить границы компонентов или по-другому передать дочерние элементы. Общее клиентское хранилище действительно нужно тогда, когда одно и то же клиентское состояние важно в нескольких удаленных частях приложения и остается таким со временем.
Самые неприятные баги появляются, когда одна и та же сущность живет в двух местах. Если панель фильтров хранит локальное состояние, а глобальное хранилище тоже отслеживает активные фильтры, они начнут расходиться. Один источник будет править, другой — врать, и UI перестанет вызывать доверие.
Прежде чем команда начнет спорить о библиотеках, договоритесь о границах состояния:
- Состояние только для UI остается локальным.
- Серверные данные остаются в слое серверного состояния.
- Общему клиентскому состоянию нужен один владелец.
Это решение помогает не только с выбором библиотеки. Оно сокращает споры, убирает дублирование состояния и делает новые функции менее болезненными в разработке.
Быстрые проверки перед тем, как добавить еще одну библиотеку
Большинство проблем управления состоянием в React выглядят страшнее, чем есть на самом деле. Команда чувствует боль от прокидывания props в React, добавляет новое хранилище, и через шесть месяцев никто не может договориться, что именно должно там лежать.
Потратьте пять минут и сначала разберите данные.
- Если значение нужно только родителю и нескольким близким дочерним компонентам, поднимите состояние и остановитесь на этом. Флаг открытия модального окна, активная вкладка или шаг формы обычно нормально работают на обычном React.
- Если данные приходят из API, относитесь к ним как к серверным данным. Обычно им важнее загрузка, кэширование, повторные попытки и правила обновления, чем клиентское хранилище.
- Если одно и то же значение могут менять из удаленных частей приложения, общее хранилище может подойти. Фильтры, которые используются на нескольких страницах, корзина или многошаговый черновик часто подходят под этот шаблон.
- Если каждое обновление запускает побочные эффекты, полезна строгая трассировка. Это может означать один понятный поток действий, предсказуемые логи и меньше загадочных обновлений во время поиска бага.
- Если новый коллега не может понять правило за минуту, настройка слишком сложная. Простые правила почти всегда лучше умных абстракций.
На маленькой продуктовой панели это особенно хорошо видно. Выбор даты и переключатель графика могут остаться локальными, если они влияют только на одну страницу. Список клиентов из бэкенда должен жить в инструменте для серверного состояния с кэшированием. Глобальное значение «selected workspace», которое используется в заголовке, сайдбаре и на странице отчетов, — как раз тот вид общего клиентского состояния, который оправдывает свое существование.
Команды часто спорят об инструментах раньше, чем договорятся о типах данных. Это наоборот. Сначала спросите, кому нужны эти данные, откуда они берутся и кто может их изменить. После этого выбор библиотеки обычно становится намного проще.
Если два варианта все еще кажутся подходящими, выбирайте тот, у которого меньше правил. Его будет быстрее отлаживать, проще объяснять и менее болезненно менять позже.
Что делать дальше
Большинству команд не нужен переписывание с нуля. Им нужна короткая карта состояния, которая называет каждый тип данных в продукте и место, где ему следует жить.
Начните с малого. Выберите один экран, который постоянно вызывает споры, лишние props или дублирующиеся запросы. Обычно хорошо видно проблему на панели, странице настроек или в редакторе.
- Выпишите каждый элемент состояния на этом экране.
- Пометьте каждый как локальное UI-состояние, серверное состояние или общее клиентское состояние.
- Отметьте, где оно живет сейчас и где должно жить вместо этого.
- Сделайте правило настолько простым, чтобы новый коллега мог понять его за пять минут.
Потом проверьте это правило на выбранном экране. Пока не меняйте все приложение. Один аккуратный проход дает больше, чем большой рефакторинг, где перемешаны старые и новые подходы.
Следите за скучными сигналами. Меньше props, передаваемых через три уровня. Меньше мест, где повторно запрашиваются одни и те же данные. Меньше комментариев в code review в духе: «Почему это в хранилище?» Если это улучшается, значит, правило, скорее всего, хорошее.
После этого зафиксируйте стандартные решения для команды. Коротко. Одной страницы по управлению состоянием в React достаточно, если она отвечает на три вопроса: когда использовать локальное состояние компонента, когда доверять кэшу запросов для серверных данных и когда оправдано общее клиентское хранилище.
Оставьте место для исключений, но помечайте их. Если экран нарушает правило, запишите почему. Так одноразовые решения не превратятся в привычку команды.
Если споры все еще мешают выпускать продукт, попросите короткий архитектурный обзор у человека со стороны. Fractional CTO вроде Oleg Sotnikov может посмотреть на одну запутанную область, разделить локальное состояние, серверное состояние и общие клиентские хранилища и дать команде набор правил, который подходит продукту. Один сфокусированный обзор может сэкономить недели бесконечных споров о хранилищах.