React Query или Redux для продуктов с серверными данными
React Query или Redux лучше работает, когда вы разделяете кэш серверных данных и состояние интерфейса. Узнайте простой способ выбрать оба инструмента без путаницы в задачах.

Почему команды застревают
Команды застревают, когда используют один стор для двух разных задач. Они складывают API-данные рядом с логикой экранов, шагами форм, фильтрами, модальными окнами и временными правками, а потом ожидают, что один инструмент будет держать всё это в порядке. Какое-то время это работает. Потом продукт растёт, и каждое изменение начинает задевать что-то постороннее.
Продукт с большим количеством серверных данных только усугубляет ситуацию. Приложение получает списки, детали, счётчики, права доступа, черновики и фоновые обновления. Одновременно интерфейс отслеживает такие вещи, как «открыт ли drawer», «какой шаг сейчас активен» или «изменил ли пользователь это поле, но ещё не сохранил». Это не один и тот же тип состояния, но команды часто хранят их одинаково.
Это быстро создаёт путаницу. Разработчик меняет фильтр в сторе, и три компонента перерисовываются. Другой разработчик сбрасывает страницу после сохранения, а приложение отправляет лишние запросы, потому что логика загрузки тоже зависит от этого общего состояния. Небольшое изменение сценария может разбудить код загрузки данных, которого никто не хотел трогать.
После нескольких таких кругов люди перестают понимать, где на самом деле лежат данные. Где последняя версия записи о клиенте: в кеше, в Redux, в локальном состоянии компонента или в каком-то скопированном результате селектора? Если команда не может ответить на это одной фразой, ошибки становится сложнее искать.
Устаревшие значения — самая болезненная часть. Экран может показывать данные, которые выглядят свежими, потому что минуту назад они пришли из глобального стора, но сервер уже всё изменил. Кто-то редактирует запись, возвращается к списку и видит старые цифры, которые всё ещё кажутся правильными. Потом другая функция читает ту же устаревшую копию и строит что-то поверх неё.
Именно поэтому споры вроде React Query или Redux часто ходят по кругу. Обычно проблема не в самом инструменте. Проблема в том, что один стор начинает выполнять и роль кэша, и роль состояния сценария, а эти две задачи живут с разной скоростью.
Когда это происходит, каждая новая функция кажется тяжелее, чем должна. Простые изменения занимают больше времени, запросов становится больше, перерисовки расползаются, и команда начинает подстраиваться под систему состояния вместо того, чтобы спокойно ей пользоваться.
Два типа состояния
Много путаницы начинается, когда команда складывает все подвижные части в одну корзину и называет её «состоянием приложения». В этой корзине обычно лежат две разные вещи, и стареют они по-разному.
Серверное состояние приходит извне приложения. API отправляет его вам, и данные могут устареть уже через минуту, потому что другой пользователь что-то изменил, фоновая задача завершилась или сервер что-то пересчитал. Сюда относятся список товаров, статус счёта, количество непрочитанных сообщений или профиль клиента.
Такому состоянию нужны кэширование, повторная загрузка, состояния загрузки, повторные попытки и понятное правило, когда устаревшие данные нужно обновлять. Если эти правила пропустить, люди начинают смотреть на старые данные и перестают доверять экрану.
Состояние рабочего процесса связано с текущей задачей пользователя. Оно показывает, что человек делает прямо сейчас: какое модальное окно открыто, на каком шаге оформления он находится, какие фильтры он выбрал перед нажатием «Применить» или какой черновик текста ещё не сохранён. Ему не нужны серверные правила свежести. Ему нужны предсказуемые обновления, простые сбросы и понятный способ двигаться по интерфейсу.
Это различие меняет то, как им стоит управлять.
- Серверное состояние сначала меняется на сервере, а потом его подхватывает интерфейс.
- Состояние рабочего процесса сначала меняется в интерфейсе, часто на каждый клик или нажатие клавиши.
- Серверное состояние может устаревать и требует логики повторной загрузки.
- Состояние рабочего процесса обычно заканчивается вместе с задачей.
Представьте панель заказов. Список заказов приходит из API, значит, ему место в кеше React Query. Открытая выдвижная панель «массовое редактирование», выбранные строки и несохранённая заметка в форме относятся к UI-состоянию Redux или к обычному состоянию компонента.
Спор React Query или Redux становится запутанным, когда команды просят один инструмент делать обе работы. React Query хорошо умеет держать загруженные данные свежими. Redux лучше подходит для состояния рабочего процесса, которое нужно нескольким частям интерфейса. Можно заставить любой из этих инструментов делать чужую работу, но код быстро становится шумным. Команды начинают писать редьюсеры для задач кэша или использовать данные запроса, чтобы управлять модальными окнами и черновиками.
Как только вы разделяете server state и workflow state, код обычно становится проще. Ошибки тоже легче объяснить, потому что можно задать один простой вопрос: проблема в устаревших серверных данных или в текущем сценарии интерфейса пользователя?
Чем должен заниматься React Query
Если источник правды — бэкенд, держите эти данные в React Query. Он хорошо подходит для всего, что вы загружаете, показываете и обновляете, когда сервер меняется.
Обычно сюда относятся:
- списки записей
- страницы одной записи
- итоги, бейджи и счётчики
- отфильтрованные или найденные результаты
Каталог товаров — простой пример. Список, страница товара, бейдж «12 позиций с низким остатком» и результаты поиска по «красные кроссовки» должны находиться в кеше React Query, потому что всё это приходит из API.
React Query также должен управлять скучными вещами, которые команды часто пишут вручную. Состояния загрузки, ошибки, повторные попытки, фоновое обновление и повторная загрузка при возврате фокуса — это его зона ответственности. Когда люди копируют эти данные в Redux, они обычно заново пишут логику, которую React Query уже предоставляет.
Ручные копии расходятся. Пользователь редактирует один элемент, страница деталей обновляется, а список всё ещё показывает старые данные, потому что кто-то забыл синхронизировать оба места. С React Query вы можете инвалидировать нужный запрос после изменения и позволить ему получить свежие данные. Это проще, чем поддерживать лишние копии.
Здесь важны настройки кеша. Используйте stale time и cache time осознанно, а не одинаково для каждого экрана. Для панели продаж могут понадобиться свежие счётчики каждую минуту, а страница настроек может спокойно лежать в кеше гораздо дольше без проблем.
Результаты поиска тоже относятся сюда, даже если они кажутся временными. Если результат приходит с сервера, держите запрос и его фильтры рядом с загрузкой. Так повторный поиск работает быстрее и не появляется второй стор, полный полуустаревших результатов.
Предзагрузка — один из самых простых выигрышей. Если пользователи часто открывают страницу деталей из таблицы, заранее подгружайте эту запись при наведении или когда строка появляется на экране. Если после оформления заказа они обычно открывают следующую вкладку, загрузите эти данные заранее. Если сделать это правильно, ощущение «пустой экран, спиннер, ожидание» заметно уменьшается.
Помогает простая проверка: если вы можете выбросить данные и снова получить их с сервера, скорее всего, ими должен владеть React Query. Пусть кэш хранит серверные данные и делает ту работу по обновлению, ради которой он и создан.
Чем должен заниматься Redux
Спор React Query или Redux становится проще, когда вы даёте каждому инструменту одну задачу. Redux должен хранить рабочую память приложения: выборы, которые человек делает в процессе задачи, особенно если эти выборы должны сохраняться между экранами, drawer'ами и общими элементами управления.
Обычно это означает прогресс и намерение, а не свежие данные с сервера. Повторная загрузка может обновить запись заказа или список товаров. Она не должна возвращать пользователя на шаг 1, закрывать панель или стирать черновик, который ещё не отправили.
Сценарий с несколькими экранами хорошо показывает разделение. Если кто-то редактирует товар через детали, цену и публикацию, React Query может загружать сохранённый товар. Redux может хранить активный шаг, несохранённые изменения и правила экрана, привязанные к этому черновику. Сохранённая запись и работа пользователя в процессе остаются раздельными.
Хорошие варианты для Redux
- текущий шаг в мастере или сценарии настройки
- несохранённые изменения формы до отправки
- какая вкладка, модальное окно, drawer или панель открыта
- правила экрана, основанные на действиях пользователя, например показать предупреждение после изменения биллинга
- решения, которые одновременно влияют на несколько компонентов
Последний случай важнее, чем многие команды ожидают. Если один выбор меняет бейдж в шапке, включает кнопку внизу и открывает боковую панель, локальное состояние компонента быстро становится запутанным. Redux даёт этим частям одно общее место, откуда можно читать данные.
Держите Redux небольшим и осознанным. Не копируйте в него целые ответы API только потому, что одним и тем же записью пользуются многие компоненты. React Query уже лучше справляется с загрузкой, кешированием, повторной загрузкой и состояниями загрузки для серверных данных.
Помогает быстрая проверка: спросите себя, описывает ли это состояние «что сервер сказал последним» или «что пользователь делает прямо сейчас». Первое место — кеш React Query. Второе обычно относится к UI-состоянию Redux.
Как разделить состояние шаг за шагом
Начните с одной функции, а не со всего приложения. Выберите что-то запутанное, например страницу заказов с фильтрами, панелью деталей, действиями сохранения и модальным окном проверки.
Запишите каждый кусочек состояния, который использует эта функция. Включите очевидные данные, но и мелочи, о которых команды забывают: выбранная вкладка, открытое модальное окно, черновые значения формы, текущий шаг, порядок сортировки, последний кликнутый элемент.
Потом отсортируйте каждый пункт по одному простому правилу: где находится правда? Если за данные отвечает бэкенд и их могут изменить многие пользователи, кладите их в кеш React Query. Если они существуют только для того, чтобы помочь одному пользователю пройти экран, держите их в UI-состоянии Redux или в локальном состоянии компонента.
- Список товаров из API, детали элемента, права доступа и счётчики идут в React Query.
- Текущий шаг мастера, несохранённые правки формы, активные фильтры и открытый drawer остаются в Redux или локальном состоянии.
- Временные флаги вроде «пользователь сейчас редактирует» или «показать предупреждение о несохранённых изменениях» тоже не должны попадать в кэш.
- Если после сохранения это нужно повторно загружать, скорее всего, это серверное состояние.
- Если после обновления страницы это должно исчезнуть, скорее всего, это состояние рабочего процесса.
После этого в первую очередь перенесите код загрузки. Создайте хуки React Query для чтения и мутаций, и пусть они отвечают за загрузку, ошибки, повторные попытки и обновления кеша. Уже это сильно сокращает количество кода в Redux.
Делайте Redux-слайсы маленькими. Слайс должен описывать пользовательский сценарий, а не копировать таблицы бэкенда. checkoutFlow звучит нормально. productsFromApi — обычно нет.
Проблемы часто возвращаются на мутациях. Не складывайте ответы сервера в Redux только потому, что кнопка сохранения там находится. Пусть мутация обновляет кеш React Query или инвалидирует его, а потом отправляет понятное UI-действие: закрыть модальное окно, сбросить черновик или перейти к следующему шагу.
Чистое разделение часто выглядит так: React Query загружает последний заказ, Redux отслеживает, что пользователь на шаге 3 и у него открыта предупреждающая плашка, а успешная мутация закрывает плашку и обновляет заказ. React Query или Redux перестаёт быть спором, когда у каждого инструмента одна задача.
Простой пример продукта
Представьте экран B2B-заказа. Менеджер по продажам выбирает клиента, проверяет остатки, видит контрактные цены, добавляет позиции, пишет заметку и проходит согласование. Всё находится на одной странице, но эта страница содержит два очень разных типа состояния.
Данные клиента, остатки и цены приходят из API. React Query должен загружать эти части отдельно и держать каждый ответ в кеше React Query. Тогда, если остаток по одному товару меняется, приложение может обновить только эти данные, не перезагружая весь экран. Запись клиента остаётся на месте. Данные о цене тоже остаются на месте, если только они не изменились.
Текущая работа менеджера должна храниться в другом месте. Redux должен содержать текущий шаг в процессе заказа, выбранные позиции до отправки, черновик заметки и мелкие UI-решения вроде того, какая панель открыта. Всё это ещё не является серверной правдой. Это просто рабочее состояние пользователя.
Чистое разделение выглядит так:
- React Query хранит клиентов, остатки, цены и сохранённые заказы
- Redux хранит активный шаг, выбранные строки, локальные фильтры и черновики заметок
- Форма читает из обоих источников, в зависимости от того, что ей нужно
- Отправка посылает черновик из Redux в API
- После успеха React Query обновляет только затронутые запросы
Допустим, менеджер выбирает клиента A и добавляет 12 единиц товара 42. React Query выдаёт актуальный остаток и правильную цену для этого клиента. Redux хранит выбранное количество и заметку «держать до пятницы» пока менеджер редактирует заказ.
Когда менеджер нажимает «Отправить», приложение посылает черновик заказа в API. Если сохранение прошло успешно, React Query может инвалидировать только изменившиеся запросы, например список сохранённых заказов, детали заказа и остаток по товару 42. Приложению не нужно перезагружать каждого клиента, каждую цену или каждый экран.
Именно здесь спор React Query или Redux обычно путается. На экране, полном серверных данных, один инструмент не должен делать обе работы. React Query работает с удалёнными данными, которые могут устареть. Redux работает со state рабочего процесса, который существует потому, что человек что-то делает прямо сейчас.
Ошибки, которые вызывают лишнюю возню
Большинство споров React Query или Redux начинаются после того, как команда смешивает две разные задачи в одном месте. Кэш должен хранить серверные данные. Redux должен хранить сценарий приложения, UI-выборы и несохранённую работу. Как только границы размываются, ошибки кажутся случайными, а уборка занимает больше времени, чем сама функция.
Распространённая ошибка — копировать данные запроса в Redux только затем, чтобы другой экран мог их читать. На словах это безобидно. На деле у вас обычно появляются две версии одного и того же клиента, заказа или проекта. React Query обновляется после повторной загрузки, а копия в Redux остаётся старой, и никто не понимает, какой версии должен доверять интерфейс.
Класть черновики форм в кеш React Query — другой источник хаоса. Черновой заголовок, несохранённая заметка или текущий шаг в процессе оформления ещё не являются серверными данными. Если фоновая повторная загрузка начнётся, пока человек печатает, кэш может заменить черновик последней сохранённой версией. Люди называют это «глючной формой», но причина в выборе хранилища.
Команды также создают лишнюю возню, когда после одного небольшого обновления обновляют каждый связанный запрос. Пользователь меняет статус с «открыто» на «закрыто», а приложение заново загружает списки, страницы деталей, счётчики, панели и боковые окна. Интерфейс мерцает, сеть шумит, а код начинает казаться хрупким. В большинстве случаев достаточно узкой инвалидaции или небольшого обновления кеша.
Ещё один источник боли — прятать побочные эффекты в случайных компонентах. Один клик по кнопке может сохранить данные, очистить слайс Redux, показать уведомление, закрыть модальное окно и перенаправить на другую страницу, но эти действия живут в трёх разных файлах. Когда что-то ломается, команда начинает гадать. Держите эти последующие действия рядом с мутацией или в одном небольшом слое действий, который всей команде легко найти.
Дрейф названий кажется мелочью, но он съедает часы. Если ключ запроса называется «customers», а Redux называет то же самое «accounts», люди перестают видеть границу между server state и workflow state. То же самое происходит, когда ключи запросов используют один формат ID, а слайсы Redux — другой. Общие названия помогают быстрее замечать ошибки.
Простое правило помогает: если это принадлежит серверу, храните это в кеше React Query. Если пользователь ещё формирует это сам, храните это в Redux или локальном состоянии. Такое разделение кажется скучным, а скучное управление состоянием обычно и переживает релизную неделю.
Быстрые проверки перед добавлением нового состояния
Слишком раннее добавление состояния создаёт большую часть путаницы. Команда хранит одно значение в React Query, копирует его в Redux, а затем добавляет поверх локальное состояние компонента. Через неделю никто не доверяет тому, что видит на экране.
Сначала остановитесь на минуту и задайте один простой вопрос: где живёт правда? Ответ обычно подскажет, кому должно принадлежать состояние — React Query или Redux.
Используйте эти проверки, прежде чем создавать новый slice, atom или запрос:
- Если источником правды является сервер, храните это значение в кеше React Query. Цена товара, количество на складе, непрочитанные сообщения и лимиты аккаунта обычно подходят сюда.
- Если сервер может изменить это без участия пользователя, не копируйте это в Redux только для удобства чтения. Загружайте, кэшируйте и при необходимости обновляйте.
- Если пользователь редактирует значение перед сохранением, считайте это черновым состоянием. Поля формы, прогресс мастера, выбранные фильтры и несохранённые заметки обычно относятся к локальному состоянию компонента или UI-состоянию Redux.
- Если значение должно пережить смену маршрута или полную перезагрузку, решите это специально. Часть состояния должна быть в URL, часть в Redux, а часть в local storage. Временный ввод часто не нуждается ни в одном из них.
- Если мутация меняет связанные данные, сразу определите путь обновления. Знайте, какой запрос вы будете инвалидировать или обновлять после сохранения, удаления или подтверждения.
Простой пример помогает. На экране заказа данные о заказе, статус оплаты и остаток на складе приходят с сервера. Они могут меняться за спиной у пользователя, поэтому ими должен владеть React Query. Открытая вкладка, несохранённая заметка о возврате и текущий шаг в workflow поддержки — это состояние сценария. Redux может хранить его, если это нужно нескольким частям интерфейса.
Если вы не можете ответить на эти проверки одной фразой каждую, не добавляйте новое состояние пока. Эта пауза потом экономит реальное время. Она также останавливает обычный спор React Query или Redux, потому что роль каждого инструмента становится гораздо яснее.
Что делать дальше
Начните с одного экрана, который вызывает ежедневное раздражение. Выберите страницу, где люди фильтруют, сортируют, редактируют и возвращаются к данным весь день. Не планируйте полный переписанный вариант. Одного занятого экрана достаточно, чтобы увидеть, делает ли ваше разделение между кешем React Query и UI-состоянием Redux код спокойнее или ещё более запутанным.
Сделайте быструю карту до того, как тронете код. Разложите каждую часть состояния на две корзины: данные, которые приходят с сервера, и временное поведение приложения, например открытые панели, черновые фильтры, шаги мастера, выбранные строки или прогресс несохранённой формы. Это небольшое упражнение часто закрывает спор React Query или Redux быстрее, чем ещё одно совещание.
Простое правило помогает: если сервер может повторно загрузить это, React Query, вероятно, должен владеть этим. Если пользователь может потерять это при обновлении страницы, и это нормально, этим может владеть Redux. Если значение существует только для того, чтобы помочь человеку закончить задачу на экране, держите его вне кеша запросов.
Запишите правила именования, пока экран ещё свеж в памяти:
- Используйте ключи запросов, которые соответствуют тому, как люди ищут данные, например product, product list или order details.
- Называйте Redux-слайсы по пользовательским сценариям, а не по API-ресурсам. Подойдут checkout flow, bulk edit или table controls.
- Держите короткую заметку о том, что никогда не попадает в Redux, например загруженные списки или детали записи.
- Держите короткую заметку о том, что никогда не попадает в React Query, например видимость модального окна или прогресс степпера.
Затем проверьте разделение на реальной задаче. Менеджер открывает страницу заказов, меняет фильтры, выбирает пять строк, открывает боковую панель и обновляет данные. Список должен оставаться в кеше запросов. Открытая панель, выбранные строки и черновик фильтра должны оставаться в состоянии рабочего процесса. Если это ощущается неудобно, границы ещё нужно доработать.
Если вашей команде нужен второй взгляд, Oleg Sotnikov может провести ревью архитектуры приложения и показать, где смешались владения состоянием. Как Fractional CTO, он помогает командам выстраивать чёткие границы, сокращать лишнее и не превращать один инструмент состояния в две разные задачи.