Кастомизация поведения RTK Query

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


Базовая точка кастомизации: createApi

Основной механизм настройки поведения RTK Query — функция createApi. Именно здесь определяется фундаментальная логика работы всего API-слоя.

Ключевые параметры конфигурации:

  • reducerPath — имя слайса в Redux store
  • baseQuery — низкоуровневый слой выполнения запросов
  • tagTypes — система инвалидирования кэша
  • endpoints — набор запросов и мутаций
  • refetchOnMountOrArgChange — глобальное поведение повторных запросов
  • keepUnusedDataFor — время жизни неиспользуемого кэша

Пример базовой конфигурации:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({
    baseUrl: '/api',
  }),
  tagTypes: ['User', 'Post'],
  keepUnusedDataFor: 30,
  refetchOnMountOrArgChange: true,
  endpoints: () => ({})
});

Каждый из этих параметров влияет на поведение системы кэширования и жизненного цикла данных.


Кастомный baseQuery как основной механизм контроля

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

fetchBaseQuery как стандарт

По умолчанию используется fetchBaseQuery, который представляет собой обёртку над fetch с добавленной логикой сериализации, обработки ошибок и заголовков.

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

  • добавление токенов авторизации
  • централизованная обработка ошибок
  • логирование запросов
  • retry-логика
  • работа с несколькими API

Расширение fetchBaseQuery

Наиболее распространённый подход — обёртка над fetchBaseQuery:

import { fetchBaseQuery } from '@reduxjs/toolkit/query/react';

const rawBaseQuery = fetchBaseQuery({
  baseUrl: '/api',
});

export const baseQueryWithAuth = async (args, api, extraOptions) => {
  const token = api.getState().auth.token;

  const result = await rawBaseQuery(
    {
      ...args,
      headers: {
        ...args.headers,
        Authorization: token ? `Bearer ${token}` : '',
      },
    },
    api,
    extraOptions
  );

  return result;
};

Здесь поведение запроса модифицируется без изменения логики RTK Query.


Полностью кастомный baseQuery

RTK Query позволяет реализовать собственный транспортный слой:

const customBaseQuery = async (args, api, extraOptions) => {
  try {
    const response = await customHttpClient.request(args);

    return { data: response.data };
  } catch (error) {
    return {
      error: {
        status: error.status,
        data: error.message,
      },
    };
  }
};

Такой подход используется при интеграции:

  • GraphQL
  • WebSocket API
  • нестандартных HTTP-клиентов (axios, ky)
  • legacy систем

Кастомизация endpoint-level поведения

Каждый endpoint в RTK Query может иметь собственную логику, отличающуюся от глобальной.

query и mutation как точки расширения

endpoints: (builder) => ({
  getUser: builder.query({
    query: (id) => `/users/${id}`,
  }),

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

Однако ключевая сила заключается в дополнительных параметрах:

  • transformResponse
  • transformErrorResponse
  • providesTags
  • invalidatesTags
  • keepUnusedDataFor
  • onQueryStarted

Преобразование данных через transformResponse

Позволяет адаптировать API-ответ под структуру приложения.

getUser: builder.query({
  query: (id) => `/users/${id}`,
  transformResponse: (response) => response.data.user,
});

Это позволяет изолировать UI от особенностей backend-структуры.


Кастомная обработка ошибок

transformErrorResponse: (response) => {
  return {
    message: response.data?.message || 'Ошибка запроса',
    status: response.status,
  };
}

Такой слой используется для нормализации ошибок независимо от API.


Управление кэшом через tags

Система тегов — центральный механизм инвалидации данных.

getPosts: builder.query({
  query: () => '/posts',
  providesTags: ['Post'],
});

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

Динамические теги

providesTags: (result) =>
  result
    ? result.map(({ id }) => ({ type: 'Post', id }))
    : ['Post'];

Это позволяет выполнять точечную инвалидацию.


Lifecycle-хуки и onQueryStarted

onQueryStarted позволяет вмешиваться в процесс запроса на этапе его запуска.

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

  async onQueryStarted(arg, { dispatch, queryFulfilled }) {
    try {
      const { data } = await queryFulfilled;
    } catch (err) {
      console.error('Ошибка обновления', err);
    }
  },
});

Оптимистические обновления

Один из наиболее мощных сценариев кастомизации:

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

  async onQueryStarted(user, { dispatch, queryFulfilled }) {
    const patchResult = dispatch(
      api.util.updateQueryData('getUser', user.id, (draft) => {
        Object.assign(draft, user);
      })
    );

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

Здесь происходит локальное изменение кэша до подтверждения сервера.


Кастомизация поведения кэша

RTK Query хранит данные в нормализованном кэше, поведение которого можно контролировать.

keepUnusedDataFor

keepUnusedDataFor: 60

Определяет, сколько секунд данные остаются в памяти после того, как последний подписчик отписался.


refetchOnMountOrArgChange

refetchOnMountOrArgChange: true

Определяет, нужно ли повторно загружать данные при повторном монтировании компонента.

Дополнительная локальная переопределяемость:

useGetUserQuery(id, {
  refetchOnMountOrArgChange: false,
});

refetchOnFocus и refetchOnReconnect

Глобальные стратегии актуализации данных:

refetchOnFocus: true,
refetchOnReconnect: true

Они обеспечивают синхронизацию данных при возвращении пользователя в приложение или восстановлении сети.


Манипуляции с кэшем через api.util

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

invalidateTags вручную

dispatch(api.util.invalidateTags(['Post']));

resetApiState

Полный сброс состояния API:

dispatch(api.util.resetApiState());

Используется при logout или смене пользователя.


Кастомизация polling поведения

RTK Query поддерживает автоматическое обновление данных через polling:

useGetMessagesQuery(undefined, {
  pollingInterval: 5000,
});

Это поведение можно динамически управлять:

  • включение/выключение через state
  • изменение интервала на лету
  • условный polling через skip

skip и conditional fetching

Механизм условного выполнения запросов:

useGetUserQuery(id, {
  skip: !id,
});

Позволяет полностью контролировать момент запуска запроса.


selectFromResult как кастомизация селектора

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

useGetUserQuery(id, {
  selectFromResult: ({ data, isLoading }) => ({
    user: data,
    loading: isLoading,
  }),
});

Это снижает нагрузку на UI при частых обновлениях кэша.


Кастомизация через middleware интеграцию

RTK Query интегрируется в Redux middleware pipeline, что позволяет расширять поведение:

  • логирование экшенов
  • аналитика запросов
  • перехват ошибок
  • синхронизация с внешними стореджами

Пример добавления middleware:

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

Динамическое изменение поведения API

RTK Query поддерживает паттерн динамической генерации API:

export const createDynamicApi = (baseUrl) =>
  createApi({
    reducerPath: 'dynamicApi',
    baseQuery: fetchBaseQuery({ baseUrl }),
    endpoints: (builder) => ({
      getData: builder.query({
        query: () => '/',
      }),
    }),
  });

Это полезно для мульти-доменных систем.


Переопределение поведения через extraOptions

extraOptions передаются в baseQuery и позволяют реализовать пользовательские режимы обработки:

const baseQuery = fetchBaseQuery({ baseUrl: '/api' });

const enhancedBaseQuery = async (args, api, extraOptions) => {
  if (extraOptions?.silent) {
    // отключение глобальных индикаторов загрузки
  }

  return baseQuery(args, api, extraOptions);
};

Сигнализация состояния и интеграция с UI-слоем

RTK Query позволяет строить сложные UI-сценарии через комбинацию:

  • isLoading
  • isFetching
  • isSuccess
  • isError

Эти флаги могут быть переопределены через кастомные селекторы и selectFromResult, что позволяет тонко контролировать UX без изменения бизнес-логики.


Разделение поведения между окружениями

Кастомизация часто используется для разделения dev/prod поведения:

const baseQuery = fetchBaseQuery({
  baseUrl: process.env.NODE_ENV === 'production'
    ? 'https://api.site.com'
    : 'http://localhost:3000',
});

Также возможно добавление логирования только в dev:

if (process.env.NODE_ENV === 'development') {
  console.log('Request:', args);
}

Расширение RTK Query через композицию

Ключевой паттерн кастомизации — композиция:

  • baseQuery + retry wrapper
  • baseQuery + auth wrapper
  • baseQuery + caching proxy
  • endpoint + optimistic updates
  • endpoint + transform layer

Каждый слой добавляет независимую ответственность, сохраняя предсказуемость поведения системы.


Изоляция доменной логики

RTK Query позволяет выносить бизнес-логику в:

  • transformResponse
  • onQueryStarted
  • кастомные hooks
  • middleware

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