State Management

RTK Query: server-state в Redux

Список задач грузится с сервера, а кнопка добавления отправляет новую задачу. После добавления список должен обновиться. На createAsyncThunk это означает thunk на загрузку, thunk на добавление, обработку трёх фаз у каждого и ручной повторный запрос списка после успешного добавления. RTK Query описывает то же самое декларативно: эндпоинт getTodos помечает кэш тегом, эндпоинт addTodo объявляет, что инвалидирует этот тег, и после добавления список перезапрашивается сам. Запрос, кэш, состояние загрузки и обновление после мутации описываются один раз в одном месте.

  • Списки и детальные карточки с сервера: загрузка, кэш и автоматическое состояние загрузки без ручных thunk
  • Мутации с обновлением связанных данных: добавил задачу, список перезапросился сам через инвалидацию тега
  • Дедупликация запросов: несколько компонентов запросили одни данные, ушёл один сетевой вызов
  • Перезапрос при возврате фокуса на вкладку и переподключении сети для свежести данных
  • Команды на Redux, которым нужен серверный кэш внутри того же store, без второй библиотеки

Предварительные знания

  • createSlice и configureStore: RTK Query подключается как reducer и middleware в тот же store
  • Жизненный цикл запроса pending/fulfilled/rejected из createAsyncThunk
  • Разница серверного и клиентского состояния: данные с сервера это кэш, а не источник истины
  • Async через createAsyncThunk

createApi и эндпоинты

createApi это единая точка описания работы с API. В ней задаётся baseQuery, общая для всех запросов основа, например базовый URL, и набор эндпоинтов. Эндпоинты делятся на два вида: query для чтения данных и mutation для их изменения. Из этого описания RTK Query сам собирает slice с кэшем, middleware для запросов и React-хуки под каждый эндпоинт.

getTodos это query: он читает список и помечает результат тегом Todo через providesTags. addTodo это mutation: он отправляет POST и через invalidatesTags объявляет, что меняет данные с тегом Todo. Дженерики задают типы: getTodos возвращает массив Todo и не принимает аргумент, addTodo возвращает Todo и принимает объект с title. Из описания RTK Query вывел готовые хуки useGetTodosQuery и useAddTodoMutation.

RTK Query не отдельная библиотека, а часть Redux Toolkit. Сгенерированный api это обычный slice с собственным reducer и middleware, которые подключаются в тот же configureStore. Кэш живёт в том же дереве состояния, что и клиентские slice, и виден в тех же Redux DevTools.

Чем отличаются эндпоинты query и mutation в createApi?

Генерируемые хуки и кэш

Сгенерированный хук query возвращает объект с данными и состоянием запроса: data, а также флаги isLoading, isFetching, isError и поле error. Компоненту не нужны ни ручной dispatch thunk, ни useEffect, ни хранение статуса. Вызов хука сам инициирует запрос при монтировании, подписывает компонент на кэш и отдаёт текущее состояние.

Ключевая выгода это кэш по ключу запроса. RTK Query строит ключ из имени эндпоинта и его аргументов. Если два компонента вызовут useGetTodosQuery, уйдёт один сетевой запрос, а оба получат один кэшированный результат. Это дедупликация: одинаковые запросы не дублируются в сети, данные шарятся через общий кэш в store.

Что даёт хукПолеНазначение
ДанныеdataКэшированный результат запроса
Первичная загрузкаisLoadingЗапрос идёт и данных ещё нет
Фоновый перезапросisFetchingИдёт обновление поверх уже имеющихся данных
ОшибкаisError, errorЗапрос завершился неудачей

Разделение isLoading и isFetching важно. isLoading это первая загрузка, когда показывают спиннер на весь блок. isFetching это фоновое обновление поверх уже показанных данных, например при возврате фокуса на вкладку: тут уместен ненавязчивый индикатор, а не сброс UI в спиннер. Эти состояния RTK Query ведёт сам.

Два компонента на странице вызывают один и тот же useGetTodosQuery без аргументов. Сколько сетевых запросов уйдёт?

Инвалидация через теги

Главный механизм связности данных в RTK Query это теги. Query через providesTags объявляет, какие теги он поставляет, то есть какой кэш он представляет. Mutation через invalidatesTags объявляет, какие теги она делает устаревшими. После успешной мутации RTK Query находит все запросы, поставлявшие инвалидированные теги, и перезапрашивает их автоматически. Ручной повторный запрос списка после добавления больше не нужен.

Теги можно делать точечными через пару type и id. Тогда обновление одной задачи инвалидирует кэш именно этой задачи, а не всего списка. Это даёт контроль над тем, что перезапрашивать: широкий тег Todo обновит всё связанное, а конкретный { type: Todo, id } только одну запись. Так инвалидация остаётся декларативной, но настолько узкой, насколько нужно.

Помимо инвалидации по тегам, RTK Query умеет перезапрашивать данные при возврате фокуса на вкладку и при переподключении сети, если включить refetchOnFocus и refetchOnReconnect. Это закрывает свежесть данных без ручных таймеров: пользователь вернулся к вкладке, и список тихо обновился в фоне через isFetching.

Что произойдёт после успешного выполнения mutation с invalidatesTags: ['Todo'], если активен query getTodos с providesTags: ['Todo']?

Серверное против клиентского состояния

RTK Query закрепляет важное разграничение. Серверное состояние это данные, чей источник истины на сервере: списки, профили, цены. Их нельзя хранить как клиентское состояние, потому что они устаревают, и нужны кэш, свежесть и инвалидация. Клиентское состояние это то, что живёт только в браузере: открытые модалки, выбранный инструмент, черновики формы. Для него достаточно обычного slice.

  • Серверное состояние (RTK Query) — Источник истины на сервере. Нужны кэш, дедупликация, инвалидация, свежесть. Описывается эндпоинтами, а не ручными редьюсерами
  • Клиентское состояние (обычный slice) — Живёт только в браузере: UI-флаги, выбор, черновики. Источник истины в самом приложении. Описывается createSlice

Частая ошибка это копировать ответ сервера в обычный slice и обновлять его вручную после каждой мутации. Так slice превращается в самописный кэш серверных данных, и за свежестью приходится следить руками. Это ровно та задача, ради которой и существует RTK Query: серверные данные держат в нём, а не дублируют в клиентских slice.

Отдельно стоит TanStack Query: он решает ту же задачу серверного кэша с инвалидацией, но вне Redux и без store. Выбор между ними скорее организационный. Если проект уже на Redux Toolkit, RTK Query держит серверный слой в том же store и тех же devtools. Если Redux в проекте нет, TanStack Query даёт серверный кэш без необходимости заводить Redux ради него.

Какие данные уместно держать в RTK Query, а не в обычном slice?

Связь с другими темами

RTK Query это серверный слой поверх ядра RTK и аналог отдельных query-библиотек:

  • Async через createAsyncThunk — RTK Query автоматизирует ровно тот цикл фаз запроса, который у thunk пишется и обрабатывается руками
  • Redux Toolkit: slices и Immer — createApi генерирует slice с кэшем и middleware, которые подключаются в тот же configureStore
  • TanStack Query — Тот же серверный кэш с инвалидацией, но вне Redux: альтернатива для проектов без Redux

Итог

  • RTK Query это слой серверного состояния внутри Redux Toolkit: createApi описывает эндпоинты, а не ручные thunk
  • Из эндпоинтов RTK Query генерирует хуки вида useGetTodosQuery и useAddTodoMutation с готовым состоянием загрузки и ошибки
  • Ответы кэшируются по ключу запроса, повторные одинаковые запросы дедуплицируются в один сетевой вызов
  • Инвалидация через теги: query помечает кэш тегом, mutation объявляет его инвалидацию, и связанные данные перезапрашиваются автоматически
  • RTK Query про серверное состояние, обычные slice про клиентское: это разные слои, которые не дублируют друг друга
  • TanStack Query решает ту же задачу серверного кэша вне Redux и служит прямым аналогом для сравнения

Связанные уроки

  • sm-12-rtk-slices — RTK Query это часть Redux Toolkit и встраивается в тот же store: без понимания slice и configureStore его не подключить
  • sm-13-rtk-async-thunks — RTK Query автоматизирует тот же цикл pending/fulfilled/rejected, который в createAsyncThunk пишется руками
  • rc-36-tanstack-query — TanStack Query решает ту же задачу серверного состояния вне Redux: прямой аналог для сравнения подходов
RTK Query: server-state в Redux

0

1

Войти