React Query и Redux: переосмысление состояния сервера в крупных приложениях
Узнайте, почему приложение для чатов в производстве использовало TanStack Query вместо Redux для управления серверными данными, и где Redux всё ещё находит своё место в современной архитектуре React.
Представьте разработчика, который переходит с мелких проектов в компанию, ориентированную на создание продуктов, и с нетерпением хочет узнать, как на самом деле создаются крупномасштабные программы промышленного уровня. После многих лет работы над фронтенд-приложениями присоединение к команде, отвечающей за программное обеспечение, используемое миллионами людей, меняет подход к оценке кода.
Вы перестаете просто спрашивать: «Работает ли эта функция?»
Вместо этого вы начинаете задавать вопросы вроде:
Как эта архитектура справляется с нагрузкой от миллионов пользователей? Как обеспечивается синхронизация состояния во всем приложении? Что происходит, когда десять разных компонентов нуждаются в одних и тех же данных? Как инженеры сохраняют управляемость таким большим кодовым базисом по мере роста продукта?
Теперь представьте, что этому разработчику поручают один из ключевых модулей продукта — чат-приложение, по сути аналогичное Slack.
Это не какая-то незначительная функция, спрятанная в уголке приложения.
Опыт общения находится в центре продукта, обслуживая миллионы пользователей и тесно связан с критически важными бизнес-процессами, механизмами генерации дохода и некоторыми из самых важных корпоративных клиентов компании.
Поэтому вполне логично, что при первом изучении кодовой базы у этого разработчика были довольно предсказуемые ожидания.
Приложение для чатов такого масштаба неизбежно работает с огромным объемом информации, которую должны обмениваться множество экранов: темы и сообщения в них, количество непрочитанных сообщений, участники разговора, просмотр истории сообщений, операции редактирования и другие записи, индикаторы загрузки и реальные обновления в режиме реального времени.
Учитывая все это, можно ожидать наличия типичных элементов в крупной кодовой базе React:
Redux, Zustand или хотя бы какая-то форма управления глобальным состоянием.
Но после просмотра кода никакого хранилища состояния не обнаружено.
Нет настроек Redux.
Нет Zustand.
Нет большого объекта Context, содержащего общие данные приложения.
На первый взгляд может показаться, что при изучении репозитория просто что-то упущено.
Однако более тщательный анализ показывает, что на самом деле отвечает за обработку общих данных.
TanStack Query.
Это действительно удивительное открытие, если ваше представление об этой библиотеке ограничено.
Многие разработчики считают React Query в основном инструментом для получения данных из API, кэширования ответов, отслеживания состояния загрузки и повторного получения данных при изменениях.
Однако в этом приложении профессионального уровня, обслуживающем миллионы пользователей, кэш запросов выполнял гораздо больше функций. Он служил общим хранилищем для всех данных, принадлежащих серверу, во всем приложении.
Это наблюдение ставит под сомнение давно сложившееся представление об архитектуре фронтенда.
Возможно, правильный вопрос не таков:
"Почему здесь нет хранилища Redux?"
Возможно, он должен звучать так:
"Зачем вообще данные, принадлежащие серверу, должны находиться в Redux?"
Этот вопрос открывает возможность для гораздо более глубокого обсуждения того, как мы моделируем состояние в приложениях на React, и почему во многих реальных системах TanStack Query может незаметно устранить значительную часть механизмов глобального состояния, которые команды традиционно создавали вручную.
От Redux к React Query
Был период, когда подключение API-запроса в React-приложение казалось простой задачей.
Затем появился Redux.
Сам API-запрос оставался простым. Всё, что было связано с ним, становилось сложным.
Нужно было определить действие.
Написать редьюсер.
Добавить флаги загрузки и ошибок.
Отправить действие.
Сохранить ответ в хранилище данных.
Написать селектор для его извлечения.
Подключить компонент ко всем этим элементам.
А через недели или месяцы кто-нибудь неизбежно спрашивает:
«Почему эти данные кажутся устаревшими?»
И решением обычно становится ещё одно действие для принудительной перезагрузки данных.
Пройдя через этот цикл многократно, становится очевидным некомфортный паттерн:
Инструменты глобального состояния часто использовались для управления тем, что изначально вовсе не являлось проблемой клиентского состояния.
Бэкенд был настоящим владельцем данных.
Фронтенд лишь потреблял их.
Именно это различие во многом объясняет, почему TanStack Query стал такой интересной альтернативой в современных React-приложениях.
Что же такое React Query?
Прежде чем рассматривать, как он сокращает необходимость в Redux, стоит прояснить распространённое заблуждение.
TanStack Query, ранее называвшийся React Query, не является заменой useState, Redux или Zustand.
Его основная задача — управление серверным состоянием — данными, исходящими извне вашего React-приложения и требующими загрузки, кэширования, синхронизации, обновления и в конечном итоге отнесения к устаревшим данным.
React Query — это не просто отправка запроса и сохранение ответа в локальное состояние компонента.
Собственная документация TanStack описывает библиотеку преимущественно в контексте получения, кэширования, синхронизации и обновления состояния сервера.
Представьте данные следующим образом:
Users
Projects
Messages
Notifications
Orders
Analytics
Ваш фронтенд на самом деле не владеет никакой из этой информации.
Владеет ею бэкенд.
React Query выступает в качестве промежуточного слоя между вашим интерфейсом и бэкендом, отвечая за жизненный цикл этих данных.
В основе этой архитектуры лежит Query Cache.
Согласно текущей документации TanStack, QueryCache представляет собой слой хранения запросов — он содержит их данные, метаданные и статус. QueryClient управляет этим кэшем и предоставляет API, которые приложение использует для чтения из него, обновления его содержимого, аннулирования записей и взаимодействия с ним в других целях.
Именно поэтому несколько независимых частей приложения могут отправлять один и тот же запрос и получать согласованные результаты:
const { data } = useQuery({
queryKey: ['projects'],
queryFn: fetchProjects
})
Однако React Query делает гораздо больше, чем просто хранит кэш одного ответа.
Он управляет кэшированием, удалением дублирующихся запросов, отслеживанием актуальности данных, фоновым обновлением информации, логикой повторных попыток, сборкой мусора, изменениями данных и аннулированием кэша — всем этим в рамках одной системы.
Например, после обновления проекта:
const queryClient = useQueryClient()
await updateProject(project)
queryClient.invalidateQueries({
queryKey: ['projects']
})
Вместо того чтобы вручную указывать десяти отдельным компонентам, как обновлять их локальную копию проекта, достаточно просто сказать системе запросов:
«Данные с сервера, лежащие в основе этого запроса, могут уже не быть актуальными».
После этого кэш самостоятельно занимается их повторной верификацией.
В этом заключается основная идея TanStack Query.
Он не пытается заменить Redux.
Это придает состоянию сервера собственный жизненный цикл.
Как только вы начинаете рассматривать состояние сервера как принципиально отличное от состояния клиента, становится гораздо проще понять, почему приложению, которое на первый взгляд кажется нуждающимся в обширном хранилище данных Redux, на самом деле может вовсе не понадобиться такое хранилище.
Redux никогда не был проблемой
Чтобы быть честным, сам Redux здесь не виноват.
Redux Toolkit по-прежнему является официально рекомендуемым подходом к работе с Redux, и Redux по-прежнему оправдывает свое существование там, где приложению действительно нужно сложное состояние с клиентской стороны, предсказуемые переходы между состояниями, пайплайны промежуточных компонентов или единая модель состояния.
Проблемы возникают, когда разработчики складывают абсолютно все в одно глобальное хранилище.
Представьте себе приложение вот такого вида:
{
user: {},
projects: [],
teams: [],
notifications: [],
orders: [],
products: [],
analytics: {},
theme: "dark",
sidebarOpen: true
}
На первый взгляд кажется, что всё это принадлежит приложению.
Однако это не так.
Попробуйте задать один простой вопрос:
Кто на самом деле владеет этими данными?
Является ли список проектов чем-то, что контролирует приложение React?
Не совсем.
Настоящим владельцем является бэкенд.
Может ли другой пользователь изменить заказ, пока вкладка вашего браузера открыта?
Конечно, может.
Может ли появиться уведомление без каких-либо действий с вашего кода React?
Да, абсолютно.
Может ли сервер односторонне отозвать или изменить права пользователя?
Без вопросов.
Следовательно, значительная часть этого «состояния приложения» вообще не принадлежит фронтенду.
Это состояние сервера.
Состояние сервера сопряжено с совершенно другой категорией проблем.
Необходимо его получать.
Необходимо его кэшировать.
Нужно определять, когда он устаревает.
Необходимо его снова получать.
После изменений необходимо поддерживать его синхронизацию.
Необходимо управлять индикаторами загрузки и состояниями ошибок.
Необходимо учитывать попытки повторной загрузки и прерванные сетевые соединения.
Именно этот набор проблем предназначен к решению TanStack Query.
Архитектурные изменения
Традиционная конфигурация, ориентированная на Redux, обычно имеет такой поток:
API
↓
Async action / thunk
↓
Reducer
↓
Redux Store
↓
Selector
↓
React Component
Для многих приложений, основанных на API, этот поток может быть преобразован в нечто подобное:
API
↓
TanStack Query
↓
Query Cache
↓
React Component
В теории разница может казаться незначительной.
Но это не так.
Настоящее изменение заключается в том, что больше не нужно вручную создавать всю инфраструктуру для управления состоянием сервера.
Возьмём что-то вроде обычного списка проектов.
В Redux обычно начинают с:
const initialState = {
data: [],
loading: false,
error: null
}
Затем добавляется асинхронное действие:
dispatch(fetchProjects())
После этого идёт логика редьюсера, которая обрабатывает:
pending
fulfilled
rejected
И, наконец, селектор:
const projects = useSelector(
state => state.projects.data
)
Теперь сравните это с версией TanStack Query:
const { data, isPending, error } = useQuery({
queryKey: ['projects'],
queryFn: fetchProjects
})
Это не просто сокращение количества строк кода.
Сам запрос становится абстракцией, оборачивающей ресурс сервера.
Имя ключа запроса указывает на ресурс, который отслеживается.
Кэш хранит результаты.
Запрос сам контролирует свой статус.
И любое количество компонентов может использовать эти же значения из кэша.
QueryCache из TanStack Query существует именно для хранения результатов запросов вместе с соответствующим состоянием, а QueryClient предоставляет интерфейс для работы с этим кэшем.
Кэш запросов по сути — это глобальное хранилище серверных данных
Это, вероятно, самая важная концепция здесь.
Многие разработчики слышат такую фразу:
"У React Query есть кэш."
И считают, что это означает:
"Это просто кэширование ответов API."
Но на самом деле всё не так просто.
Кэш запросов фактически становится общим, единственным источником правды для серверного состояния вашего приложения.
Представьте три отдельных компонента:
Dashboard
|
+── ProjectList
|
+── ProjectSidebar
|
+── RecentProjects
Каждому из них нужны одинаковые данные, идентифицируемые по:
['projects']
Нет необходимости вручную передавать данные с сервера в Redux, а затем заставлять все три компонента читать их из хранилища.
Достаточно отправлять одинаковый запрос там, где он нужен:
useQuery({
queryKey: ['projects'],
queryFn: fetchProjects
})
TanStack Query самостоятельно занимается обменом и кэшированием этих данных на фоне.
Результативная архитектура может выглядеть следующим образом:
React App
|
┌─────────┴─────────┐
| |
Client State Server State
| |
Redux / Zustand TanStack Query
| |
UI state Query Cache
Внезапно вся система становится гораздо проще для понимания.
Может ли React Query заменить Redux?
В некоторых приложениях да, это действительно возможно.
Но вот ключевая нюансность:
Он не заменяет Redux, будучи его улучшенной версией.
Вместо этого он устраняет необходимость использования Redux для хранения состояния сервера с самого начала.
Это совершенно другое утверждение.
В собственной документации TanStack Query прямо указывается, что управление состоянием сервера представляет собой отдельную проблему по сравнению с управлением состоянием клиента, и отмечается, что как только состояние сервера переходит в React Query, объем состояния клиента, которое ещё нужно управлять глобально, может значительно сократиться.
Именно здесь с архитектурной точки зрения всё становится по-настоящему интересным.
Возможен такой подход к разделению:
Client State
theme
sidebar
selectedTab
modal
editor
filters
Помимо этого:
Server State
users
projects
orders
notifications
products
analytics
В этот момент выбор инструментов становится гораздо более целенаправленным:
Client State → Redux / Zustand / Context / React
Server State → TanStack Query
Вместо того чтобы ожидать, что один хранилище будет выполнять обе функции одновременно.
Но у Redux всё ещё есть задачи
Рассмотрим другой тип приложения, более похожий на инструмент дизайна вроде Figma.
Возможно, придётся хранить такое состояние:
{
selectedLayer,
activeTool,
zoom,
canvasMode,
dragState,
undoStack,
redoStack
}
Ничто из этого не является состоянием сервера.
Фронтенд полностью контролирует его.
Оно изменяется синхронно, непосредственно в ответ на действия пользователя.
Множество компонентов интерфейса одновременно зависят от него.
Иногда требуется координировать сложные переходы, когда одно состояние влияет на другое.
Именно в таких ситуациях пригоден специализированный менеджер состояния клиента.
В документации TanStack Query также говорится о том, что сложное состояние интерфейса, не связанное с сервером, тоже может стать основанием для использования специального инструмента управления состоянием клиента.
Следовательно, вывод не в том, чтобы «удалить Redux повсюду и заменить его на React Query». Это было бы чрезмерной корректировкой.
Ещё один важный игрок: RTK Query
Есть ещё один аспект, который стоит упомянуть.
Redux Toolkit уже включает в себя RTK Query — слой для получения данных и кэширования, разработанный специально для работы внутри приложений на Redux.
Он может автоматически генерировать хуки и заниматься получением данных с конечных точек, отображением состояний загрузки и кэшированием, примерно так же, как это делает TanStack Query.
Следовательно, реальные изменения в этой экосистеме заключаются не просто в:
Redux → React Query
Это скорее выглядит как такой процесс развития:
Manual API state in Redux
↓
Dedicated server-state solutions
↓
TanStack Query / RTK Query / Apollo / SWR
Вся индустрия постепенно осознаёт, что состояние сервера и состояние клиента — это фундаментально разные аспекты, требующие разных инструментов.
Как только вы усвоите это различие, станет гораздо проще понимать общую архитектуру состояния.
Правило, которое я сейчас использую
При принятии решения о том, следует ли какой-либо данные в глобальное состояние, достаточно ответить на один вопрос:
Кто на самом деле владеет этими данными?
Если ответ таков:
Бэкенд
то вы, скорее всего, имеете дело со состоянием сервера.
Если ответ таков:
Фронтенд
то вы, скорее всего, имеете дело со состоянием клиента.
Этот один вопрос обычно указывает на совершенно другую архитектуру в зависимости от ситуации.
Например:
Current user ────────── Server
Projects ────────────── Server
Orders ──────────────── Server
Notifications ───────── Server
Theme ───────────────── Client
Modal ───────────────── Client
Selected tab ────────── Client
Editor state ────────── Client
Как только вы представите ситуацию в таком виде, правильная структура становится очевидной.
React Query не уничтожает Redux
Утверждение о том, что «React Query заменяет Redux», является несколько вводящим в заблуждение.
На самом деле происходит что-то более тонкое:
Разработчики становятся лучше в определении того, с какой категорией состояния они на самом деле имеют дело.
Раньше Redux считался стандартным местом хранения всего, включая ответы сервера.
Все чаще команды разделяют:
Server state
↓
TanStack Query
из:
Client state
↓
Redux / Zustand / Context / React
Для многих современных проектов на React одно только такое разделение устраняет значительную часть сложности, которая раньше приписывалась самому Redux.
Цель не в том, чтобы сократить количество используемых библиотек.
Цель — перестать вручную создавать инфраструктуру для проблем, у которых уже существуют надежные, специально разработанные абстракции.
Поэтому в следующий раз, когда вы столкнетесь с перегруженным компонентом Redux, наполненным ответами API, флагами загрузки, логикой аннулирования кэша и действиями перезагрузки, спросите себя:
Действительно ли вам нужен глобальный менеджер состояния для этого, или вы просто переимплементировали React Query вручную внутри Redux?
Как вы решаете эту проблему в своих приложениях?
Храните ли вы в настоящее время данные сервера в Redux или Zustand, используете TanStack Query или прибегаете к совершенно другому подходу?
Связанные статьи
- Десять скрытых проблем компонентов React, замедляющих работу современных приложений — Узнайте о десяти распространенных ошибках в компонентах React — от пробелов в семантическом HTML до отсутствия мемоизации — и о способах их устранения для обеспечения высокой скорости, доступности и отсутствия ошибок в приложениях к 2026 году.
- Устранение перегрузки свойствами в React с помощью композиции и слотов — Узнайте, почему свойства React с большим количеством настроек приводят к проблемам с техническим обслуживанием, и как инверсия контроля, композиция и слоты позволяют создавать по-настоящему переиспользуемые компоненты.