Основные преимущества перед другими решениями

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

  • глобальное хранилище;
  • кеширование;
  • управление запросами;
  • синхронизацию состояния;
  • middleware;
  • DevTools;
  • генерацию хуков;
  • автоматическую типизацию.

Такой подход уменьшает количество инфраструктурного кода и устраняет необходимость комбинировать несколько библиотек одновременно.

Пример типичной конфигурации:

import { configureStore } from '@reduxjs/toolkit'
import { api } from './services/api'

export const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(api.middleware)
})

В большинстве альтернативных решений приходится отдельно реализовывать:

  • storage;
  • middleware;
  • invalidation;
  • retry-логику;
  • синхронизацию запросов;
  • статус загрузки.

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


Минимальное количество boilerplate-кода

Одним из главных преимуществ RTK Query является сокращение шаблонного кода.

Классический Redux-подход требует:

  • action types;
  • action creators;
  • thunk/saga;
  • reducers;
  • selectors;
  • состояния loading/error/data;
  • ручного обновления store.

RTK Query автоматически генерирует значительную часть инфраструктуры.

Обычный Redux + thunk

export const fetchUsers = () => async (dispatch) => {
  dispatch({ type: 'users/loading' })

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

    dispatch({
      type: 'users/success',
      payload: data
    })
  } catch (error) {
    dispatch({
      type: 'users/error',
      payload: error.message
    })
  }
}

RTK Query

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

export const {
  useGetUsersQuery
} = api

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


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

RTK Query предоставляет встроенную систему кеширования без дополнительной настройки.

После первого запроса данные сохраняются в store и переиспользуются:

const { data } = useGetUsersQuery()

При повторном использовании того же query:

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

Большинство альтернатив требуют ручной реализации кеширующего слоя либо сложной конфигурации.


Умная инвалидация кеша

Одним из наиболее сильных механизмов RTK Query считается tag-based invalidation.

Пример:

const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({
    baseUrl: '/api'
  }),
  tagTypes: ['Users'],
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => '/users',
      providesTags: ['Users']
    }),

    createUser: builder.mutation({
      query: (body) => ({
        url: '/users',
        method: 'POST',
        body
      }),
      invalidatesTags: ['Users']
    })
  })
})

После выполнения createUser:

  • RTK Query автоматически помечает кеш устаревшим;
  • нужные запросы обновляются;
  • UI синхронизируется без ручного dispatch.

Во многих других решениях разработчику приходится самостоятельно:

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

Автоматическая генерация React-хуков

RTK Query генерирует хуки автоматически:

export const {
  useGetUsersQuery,
  useCreateUserMutation
} = api

Преимущества:

  • единообразный API;
  • отсутствие ручных custom hooks;
  • минимизация ошибок;
  • ускорение разработки;
  • предсказуемая структура проекта.

Пример использования:

function Users() {
  const {
    data,
    isLoading,
    error
  } = useGetUsersQuery()

  if (isLoading) {
    return <div>Loading...</div>
  }

  if (error) {
    return <div>Error</div>
  }

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>
          {user.name}
        </li>
      ))}
    </ul>
  )
}

Централизованное описание API

RTK Query позволяет хранить все endpoints в едином месте:

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

    getPosts: builder.query({
      query: () => '/posts'
    }),

    getComments: builder.query({
      query: () => '/comments'
    })
  })
})

Это создаёт:

  • прозрачную архитектуру;
  • единый источник истины;
  • упрощённую поддержку;
  • предсказуемую структуру API-слоя.

В проектах с большим количеством REST endpoints это особенно важно.


Снижение количества запросов

RTK Query автоматически предотвращает дублирующиеся запросы.

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

useGetUsersQuery()

то выполняется:

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

Это снижает:

  • нагрузку на backend;
  • количество повторных запросов;
  • вероятность race conditions.

Автоматическое управление жизненным циклом запросов

RTK Query самостоятельно отслеживает:

  • подписки компонентов;
  • активность query;
  • удаление неиспользуемого кеша;
  • refetch;
  • polling;
  • reconnect;
  • focus events.

Пример автоматического обновления данных:

const { data } = useGetUsersQuery(undefined, {
  pollingInterval: 5000
})

Пример обновления при возврате на вкладку:

const { data } = useGetUsersQuery(undefined, {
  refetchOnFocus: true
})

Подобные возможности в обычном Redux требуют значительного объёма дополнительной логики.


Встроенная поддержка optimistic updates

RTK Query поддерживает optimistic updates на уровне API.

Пример:

updateUser: builder.mutation({
  query: ({ id, ...patch }) => ({
    url: `/users/${id}`,
    method: 'PATCH',
    body: patch
  }),

  async onQueryStarted(arg, { dispatch, queryFulfilled }) {
    const patchResult = dispatch(
      api.util.updateQueryData(
        'getUsers',
        undefined,
        (draft) => {
          const user = draft.find(
            user => user.id === arg.id
          )

          Object.assign(user, arg)
        }
      )
    )

    try {
      await queryFulfilled
    } catch {
      patchResult.undo()
    }
  }
})

Преимущества:

  • мгновенный UI;
  • откат изменений при ошибке;
  • уменьшение latency perception;
  • улучшение UX.

Гибкая система запросов

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

  • REST;
  • GraphQL;
  • кастомные transport layer;
  • WebSocket;
  • streaming;
  • авторизацию;
  • refresh token;
  • retry;
  • interceptors.

Пример кастомного baseQuery:

const customBaseQuery = async (
  args,
  api,
  extraOptions
) => {
  const result = await fetchBaseQuery({
    baseUrl: '/api'
  })(args, api, extraOptions)

  if (result.error?.status === 401) {
    // refresh token logic
  }

  return result
}

Это позволяет адаптировать RTK Query под сложные enterprise-приложения.


Предсказуемость архитектуры

RTK Query навязывает единый подход к работе с серверным состоянием.

Во многих командах это устраняет:

  • хаотичные fetch-запросы;
  • разрозненные custom hooks;
  • дублирование API-логики;
  • неконсистентный state management.

Типичная структура:

src/
├── app/
├── services/
│   └── api.js
├── features/
├── components/
└── pages/

Единый паттерн особенно полезен:

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

Простая интеграция с TypeScript

RTK Query предоставляет мощный inference типов.

Пример:

getUsers: builder.query<User[], void>({
  query: () => '/users'
})

Типы автоматически распространяются:

  • на response;
  • hooks;
  • mutations;
  • selectors;
  • cache utilities.

Многие альтернативные решения требуют более сложной типизации.


Встроенные статусы запросов

RTK Query автоматически предоставляет:

  • isLoading;
  • isFetching;
  • isSuccess;
  • isError;
  • error;
  • data;
  • currentData.

Пример:

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

Это устраняет необходимость вручную хранить статус запросов в Redux store.


Возможность lazy-запросов

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

const [
  trigger,
  result
] = useLazyGetUsersQuery()

Пример:

<button onCl ick={() => trigger()}>
  Load users
</button>

Преимущества:

  • загрузка по требованию;
  • уменьшение initial load;
  • гибкое управление сетевыми запросами.

Повторное использование endpoints

Endpoints можно расширять и инжектировать динамически:

const extendedApi = api.injectEndpoints({
  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => '/posts'
    })
  })
})

Это важно для:

  • code splitting;
  • microfrontend-архитектуры;
  • modular architecture;
  • lazy loading.

Встроенная поддержка SSR

RTK Query поддерживает server-side rendering.

Особенно полезно для:

  • Next.js;
  • SEO;
  • hydration;
  • prefetching.

Пример prefetch:

store.dispatch(
  api.endpoints.getUsers.initiate()
)

Это облегчает интеграцию с серверным рендерингом без дополнительной инфраструктуры.


DevTools и прозрачность состояния

Поскольку RTK Query использует Redux store, все запросы видны через Redux DevTools.

Можно отслеживать:

  • query lifecycle;
  • mutations;
  • cache state;
  • invalidation;
  • payload;
  • timing.

Это значительно упрощает:

  • debugging;
  • profiling;
  • анализ сетевой активности.

Экономия памяти и ресурсов

RTK Query автоматически очищает неиспользуемый кеш.

Пример:

keepUnusedDataFor: 60

После отсутствия подписчиков данные удаляются.

Преимущества:

  • контролируемое потребление памяти;
  • отсутствие бесконечного роста кеша;
  • автоматическое управление lifecycle.

Снижение количества ошибок

RTK Query уменьшает число типичных ошибок:

  • забытый dispatch;
  • неправильный loading-state;
  • гонки запросов;
  • рассинхронизация кеша;
  • дублирование запросов;
  • stale data;
  • несогласованное состояние.

Причина заключается в декларативной модели работы.

Вместо императивного управления состоянием:

dispatch(fetchUsers())

используется декларативный запрос:

useGetUsersQuery()

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


Удобство масштабирования

На ранних этапах проекта RTK Query остаётся простым:

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

Но по мере роста приложения библиотека позволяет:

  • добавлять optimistic updates;
  • подключать streaming;
  • использовать websocket;
  • внедрять pagination;
  • реализовывать infinite scroll;
  • управлять сложным кешированием;
  • разделять API на модули.

Это делает RTK Query универсальным решением как для небольших SPA, так и для крупных enterprise-систем.