Что такое RTK Query и зачем он нужен

RTK Query — это инструмент для получения, кеширования и синхронизации данных с сервером, входящий в состав библиотеки Redux Toolkit. Он решает одну из самых сложных задач фронтенд-разработки: управление серверным состоянием.

Под серверным состоянием понимаются данные, которые:

  • хранятся на удалённом сервере;
  • загружаются по HTTP;
  • могут изменяться другими пользователями;
  • требуют актуализации;
  • кешируются;
  • имеют статусы загрузки и ошибок.

Типичные примеры:

  • список товаров;
  • профиль пользователя;
  • комментарии;
  • уведомления;
  • результаты поиска;
  • статистика;
  • данные административной панели.

До появления RTK Query разработчики обычно писали большое количество однотипного кода:

const fetchUsers = () => async (dispatch) => {
  dispatch(setLoading(true))

  try {
    const response = await fetch('/api/users')
    const data = await response.json()

    dispatch(setUsers(data))
  } catch (error) {
    dispatch(setError(error.message))
  } finally {
    dispatch(setLoading(false))
  }
}

При увеличении количества API-запросов приложение быстро обрастало:

  • множеством action;
  • reducer;
  • thunk;
  • флагами loading/error;
  • ручным кешированием;
  • дублирующейся логикой;
  • сложной синхронизацией данных.

RTK Query автоматизирует практически все эти задачи.


Главная идея RTK Query

RTK Query строится вокруг концепции декларативной работы с API.

Разработчик описывает:

  • какие endpoint существуют;
  • какие данные они получают;
  • какие данные возвращают;
  • какие сущности кешируются;
  • какие данные должны обновляться.

Всё остальное библиотека берёт на себя:

  • выполнение HTTP-запросов;
  • кеширование;
  • дедупликацию запросов;
  • управление loading/error состояниями;
  • повторные запросы;
  • автоматическую инвалидацию кеша;
  • синхронизацию между компонентами;
  • генерацию React hooks.

Почему обычный Redux плохо подходит для серверного состояния

Redux отлично справляется с клиентским состоянием:

  • переключение темы;
  • состояние модальных окон;
  • локальные настройки;
  • выбранные фильтры;
  • состояние интерфейса.

Но серверное состояние имеет другую природу.

Особенности серверного состояния

Асинхронность

Данные загружаются не мгновенно.

Необходимо хранить:

{
  data: [],
  isLoading: true,
  error: null
}

Актуальность данных

Серверные данные могут устаревать.

Например:

  • пользователь открыл вкладку;
  • через 10 минут данные уже не актуальны;
  • требуется refetch.

Кеширование

Если два компонента запрашивают один и тот же список пользователей, нет смысла выполнять два одинаковых HTTP-запроса.


Синхронизация

После создания нового комментария список комментариев должен автоматически обновиться.


Жизненный цикл запросов

Необходимо учитывать:

  • отмену запросов;
  • повторные запросы;
  • race conditions;
  • background refetch;
  • polling;
  • focus refetch;
  • reconnect refetch.

Ручная реализация подобных механизмов превращается в сложную инфраструктуру.

RTK Query предоставляет готовое решение.


История появления RTK Query

До RTK Query экосистема Redux обычно использовала:

  • redux-thunk;
  • redux-saga;
  • redux-observable;
  • axios + thunk;
  • custom middleware.

Для работы с серверным состоянием популярность получили отдельные библиотеки:

  • React Query;
  • SWR;
  • Apollo Client;
  • Relay.

Команда Redux Toolkit решила встроить аналогичное решение непосредственно в Redux-экосистему.

Так появился RTK Query.


Какие проблемы решает RTK Query

Удаление boilerplate-кода

Без RTK Query для каждого запроса обычно создаются:

  • action types;
  • action creators;
  • thunk;
  • reducer;
  • selectors;
  • loading flags;
  • error handlers.

RTK Query избавляет от этой рутины.

Вместо десятков файлов:

getUsers: builder.query({
  query: () => '/users'
})

Автоматическое кеширование

RTK Query автоматически кеширует ответы.

Если один и тот же запрос вызывается повторно:

useGetUsersQuery()

библиотека:

  • не отправит повторный запрос;
  • вернёт данные из кеша;
  • синхронизирует подписчиков.

Дедупликация запросов

Если несколько компонентов одновременно вызывают одинаковый endpoint:

useGetUsersQuery()

RTK Query выполнит только один HTTP-запрос.

Все компоненты получат общий результат.


Автоматическое управление состояниями

Каждый query автоматически предоставляет:

const {
  data,
  error,
  isLoading,
  isFetching,
  isSuccess,
  isError
} = useGetUsersQuery()

Не требуется создавать reducer вручную.


Инвалидация кеша

После изменения данных RTK Query умеет автоматически обновлять связанные запросы.

Пример:

addPost: builder.mutation({
  query: (body) => ({
    url: '/posts',
    method: 'POST',
    body
  }),
  invalidatesTags: ['Posts']
})

После создания записи:

  • кеш списка постов инвалидируется;
  • список автоматически перезагружается;
  • интерфейс синхронизируется с сервером.

Генерация hooks

RTK Query автоматически создаёт hooks:

const { data } = useGetUsersQuery()

и mutations:

const [createUser] = useCreateUserMutation()

Разработчику не нужно писать их вручную.


Архитектура RTK Query

RTK Query состоит из нескольких ключевых частей.


API Slice

Главный объект RTK Query — API Slice.

Создаётся через:

createApi()

Пример:

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({
    baseUrl: '/api'
  }),
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => '/users'
    })
  })
})

API Slice:

  • хранит endpoint;
  • управляет кешем;
  • создаёт reducer;
  • создаёт middleware;
  • генерирует hooks.

Endpoint

Endpoint описывает конкретный запрос.

Существует два типа endpoint:

Query

Для получения данных:

builder.query()

Примеры:

  • GET /users
  • GET /posts
  • GET /profile

Mutation

Для изменения данных:

builder.mutation()

Примеры:

  • POST
  • PUT
  • PATCH
  • DELETE

Base Query

Base Query — базовый механизм выполнения запросов.

Стандартный вариант:

fetchBaseQuery()

Это обёртка над Fetch API.

Пример:

baseQuery: fetchBaseQuery({
  baseUrl: '/api'
})

Что умеет fetchBaseQuery

Подстановка base URL

baseUrl: '/api'

Заголовки

prepareHeaders: (headers) => {
  headers.set('Authorization', `Bearer ${token}`)

  return headers
}

JSON serialization

RTK Query автоматически:

  • сериализует body;
  • вызывает JSON.stringify;
  • парсит JSON-ответы.

Обработка ошибок

Ошибки автоматически преобразуются в единый формат.


Жизненный цикл query

Когда компонент вызывает:

useGetUsersQuery()

RTK Query выполняет следующие действия:

  1. Проверяет наличие данных в кеше.
  2. Проверяет актуальность кеша.
  3. Выполняет запрос при необходимости.
  4. Сохраняет результат.
  5. Подписывает компонент на изменения.
  6. Управляет loading/error состояниями.
  7. Удаляет неиспользуемые данные через cache lifetime.

Кеширование в RTK Query

Кеш — одна из главных возможностей библиотеки.


Как работает кеш

Каждый запрос получает уникальный cache key.

Например:

useGetPostQuery(5)

Ключ может выглядеть так:

getPost(5)

RTK Query хранит:

{
  queries: {
    'getPost(5)': {
      data: {...},
      status: 'fulfilled'
    }
  }
}

Shared Cache

Если разные компоненты используют одинаковый query:

useGetPostQuery(5)

они получают общие данные.

Это снижает:

  • количество HTTP-запросов;
  • нагрузку на сервер;
  • количество ререндеров.

Cache Lifetime

RTK Query удаляет неиспользуемые данные не сразу.

Настройка:

keepUnusedDataFor: 60

означает:

  • данные будут жить 60 секунд;
  • после отписки последнего компонента кеш сохранится;
  • повторное открытие страницы не вызовет новый запрос.

Refetch механизмы

RTK Query умеет автоматически обновлять данные.


Refetch on Focus

refetchOnFocus: true

Когда пользователь возвращается во вкладку:

  • запрос автоматически обновляется.

Refetch on Reconnect

refetchOnReconnect: true

После восстановления интернета:

  • данные автоматически перезагружаются.

Polling

Регулярный автоматический refetch:

pollingInterval: 5000

Запрос будет выполняться каждые 5 секунд.


Разница между isLoading и isFetching

Это одна из самых важных особенностей RTK Query.


isLoading

isLoading === true

означает:

  • запрос выполняется впервые;
  • данных ещё нет.

Обычно используется для initial loader.


isFetching

isFetching === true

означает:

  • запрос обновляется;
  • старые данные уже существуют.

Интерфейс может продолжать показывать старые данные без loader reset.


Mutation в RTK Query

Mutation используются для изменения данных.

Пример:

createPost: builder.mutation({
  query: (post) => ({
    url: '/posts',
    method: 'POST',
    body: post
  })
})

Использование:

const [createPost, result] =
  useCreatePostMutation()

Что предоставляет mutation

RTK Query автоматически даёт:

const [
  createPost,
  {
    data,
    error,
    isLoading,
    isSuccess
  }
]

Optimistic Updates

RTK Query поддерживает optimistic update.

Суть подхода:

  1. интерфейс обновляется сразу;
  2. запрос отправляется позже;
  3. при ошибке изменения откатываются.

Это создаёт ощущение мгновенного интерфейса.


Tags и инвалидация

Одно из важнейших понятий RTK Query — tags.


Provides Tags

Query может объявить:

providesTags: ['Posts']

Это означает:

  • query предоставляет данные типа Posts.

Invalidates Tags

Mutation может объявить:

invalidatesTags: ['Posts']

После mutation:

  • RTK Query найдёт все query с tag Posts;
  • автоматически выполнит refetch.

Почему tags важны

Без tags разработчик должен вручную:

  • обновлять store;
  • синхронизировать списки;
  • обновлять detail pages;
  • инвалидировать кеш;
  • избегать race conditions.

RTK Query делает это автоматически.


Нормализация данных

RTK Query не требует обязательной нормализации.

Это важное отличие от классического Redux.

Ранее разработчики часто создавали:

{
  users: {
    byId: {},
    allIds: []
  }
}

RTK Query допускает хранение данных в исходном виде.

Однако при необходимости можно использовать:

createEntityAdapter()

для нормализованных структур.


RTK Query и React

RTK Query особенно тесно интегрирован с React.

Библиотека автоматически генерирует hooks:

useGetUsersQuery()
useAddUserMutation()

Это создаёт очень компактный код.


Сравнение обычного Redux и RTK Query

Обычный Redux

dispatch(fetchUsers())

Нужно отдельно:

  • писать thunk;
  • reducer;
  • selectors;
  • loading state;
  • error state.

RTK Query

const { data } = useGetUsersQuery()

Практически вся инфраструктура скрыта внутри библиотеки.


RTK Query и React Query

RTK Query часто сравнивают с React Query.


Общие черты

Обе библиотеки поддерживают:

  • кеширование;
  • background refetch;
  • mutations;
  • polling;
  • retry;
  • deduplication;
  • optimistic updates.

Отличия

RTK Query

Лучше подходит для:

  • Redux-приложений;
  • централизованной архитектуры;
  • единого store;
  • строгой типизации Redux-экосистемы.

React Query

Лучше подходит для:

  • приложений без Redux;
  • независимого server-state слоя;
  • более гибкой архитектуры.

Когда RTK Query особенно полезен

Большие SPA

Где присутствует:

  • большое количество API;
  • сложная синхронизация;
  • shared cache;
  • административные панели.

Enterprise-приложения

RTK Query хорошо показывает себя в:

  • CRM;
  • ERP;
  • BI-системах;
  • dashboards;
  • внутренних корпоративных сервисах.

Redux-first архитектура

Если приложение уже использует Redux:

RTK Query становится естественным продолжением архитектуры.


Когда RTK Query может быть избыточным

Не каждое приложение нуждается в RTK Query.


Простые сайты

Например:

  • лендинги;
  • статические сайты;
  • небольшие формы.

Иногда достаточно обычного fetch.


Маленькие приложения без Redux

Если Redux отсутствует:

  • React Query;
  • SWR;
  • обычный fetch

могут быть проще.


Что хранит RTK Query внутри store

RTK Query создаёт отдельный slice:

{
  api: {
    queries: {},
    mutations: {},
    subscriptions: {},
    provided: {}
  }
}

Queries

Содержат:

  • кешированные данные;
  • статусы;
  • timestamps;
  • аргументы запросов.

Mutations

Хранят:

  • статусы mutation;
  • результаты;
  • ошибки.

Subscriptions

RTK Query отслеживает:

  • какие компоненты подписаны;
  • какие query используются;
  • когда можно очистить кеш.

Middleware RTK Query

RTK Query активно использует middleware.

Middleware отвечает за:

  • запросы;
  • refetch;
  • polling;
  • cache cleanup;
  • invalidation;
  • lifecycle management.

SSR и RTK Query

RTK Query поддерживает SSR.

Это особенно важно для:

  • Next.js;
  • server rendering;
  • SEO-страниц.

Можно:

  • предварительно загружать данные;
  • гидратировать кеш;
  • избегать повторных запросов на клиенте.

Streaming и WebSocket

Хотя RTK Query ориентирован на HTTP, он поддерживает:

  • WebSocket integration;
  • streaming updates;
  • custom baseQuery;
  • subscription patterns.

Почему RTK Query считается современным подходом

RTK Query отражает современную эволюцию frontend-разработки.

Главная идея:

серверное состояние — отдельная категория данных, требующая специализированного инструмента.

RTK Query предоставляет:

  • декларативность;
  • автоматизацию;
  • кеширование;
  • синхронизацию;
  • минимизацию boilerplate;
  • predictable data flow;
  • интеграцию с Redux;
  • масштабируемую архитектуру.

Вместо ручного управления запросами разработчик описывает структуру API, а библиотека берёт на себя инфраструктурные задачи работы с серверными данными.