Стандартный fetchBaseQuery

fetchBaseQuery — стандартный базовый механизм выполнения HTTP-запросов в RTK Query. Он построен поверх встроенного браузерного API fetch и предоставляет минималистичный, но гибкий инструмент для взаимодействия с REST API.

Основная задача fetchBaseQuery — избавить от ручного написания асинхронной логики, обработки состояний загрузки, сериализации запросов и преобразования ответов.

Базовый пример:

import { createApi, fetchBaseQuery } fr om '@reduxjs/toolkit/query/react'

export const api = createApi({
    reducerPath: 'api',
    baseQuery: fetchBaseQuery({
        baseUrl: 'https://jsonplaceholder.typicode.com',
    }),
    endpoints: (builder) => ({
        getPosts: builder.query({
            query: () => '/posts',
        }),
    }),
})

export const {
    useGetPostsQuery,
} = api

В данном примере:

  • fetchBaseQuery создаёт базовый HTTP-клиент;
  • baseUrl задаёт общий адрес API;
  • endpoint возвращает относительный путь;
  • RTK Query автоматически объединяет URL.

Полный адрес запроса:

https://jsonplaceholder.typicode.com/posts

Архитектура fetchBaseQuery

fetchBaseQuery представляет собой фабрику функций.

Вызов:

fetchBaseQuery(options)

возвращает функцию:

(args, api, extraOptions) => Promise<Result>

Эта функция затем автоматически используется RTK Query внутри всех endpoint.

Схема работы:

component
   ↓
hook
   ↓
endpoint
   ↓
query()
   ↓
fetchBaseQuery()
   ↓
fetch()
   ↓
server

Подключение fetchBaseQuery

Импорт:

import { fetchBaseQuery } fr om '@reduxjs/toolkit/query/react'

Либо:

import { fetchBaseQuery } fr om '@reduxjs/toolkit/query'

Разница:

  • /react включает React-hooks;
  • базовый пакет содержит только core-функциональность.

Базовая конфигурация

Минимальный вариант:

fetchBaseQuery({
    baseUrl: 'https://api.example.com',
})

Чаще используется расширенная конфигурация:

fetchBaseQuery({
    baseUrl: 'https://api.example.com',
    prepareHeaders,
    fetchFn,
    paramsSerializer,
    timeout,
    credentials,
    mode,
    cache,
    redirect,
    referrerPolicy,
})

Свойство baseUrl

Общий URL API

baseUrl задаёт префикс для всех запросов.

Пример:

fetchBaseQuery({
    baseUrl: 'https://api.site.com/api/v1',
})

Endpoint:

query: () => '/users'

Результат:

https://api.site.com/api/v1/users

Использование без ведущего /

Допустимы оба варианта:

query: () => 'users'

и:

query: () => '/users'

Но рекомендуется придерживаться единого стиля во всём проекте.


Динамический baseUrl

Иногда API зависит от окружения:

const baseQuery = fetchBaseQuery({
    baseUrl: process.env.API_URL,
})

Для Vite:

baseUrl: import.meta.env.VITE_API_URL

Описание запроса через query

Короткая форма

Простейший вариант:

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

RTK Query автоматически создаёт GET-запрос.


Полная форма

Можно вернуть объект:

getUsers: builder.query({
    query: () => ({
        url: '/users',
        method: 'GET',
    }),
})

Поля объекта запроса

url

Адрес endpoint:

url: '/posts'

method

HTTP-метод:

method: 'POST'

Поддерживаются:

  • GET
  • POST
  • PUT
  • PATCH
  • DELETE
  • OPTIONS

body

Тело запроса:

body: {
    title: 'New post',
}

params

Query-параметры URL:

params: {
    page: 1,
    lim it: 10,
}

Результат:

?page=1&limit=10

headers

Локальные заголовки:

headers: {
    Authorization: 'Bearer token',
}

Полный пример

createPost: builder.mutation({
    query: (post) => ({
        url: '/posts',
        method: 'POST',
        body: post,
        params: {
            notify: true,
        },
        headers: {
            'X-App-Version': '1.0',
        },
    }),
})

Автоматическая сериализация JSON

Одно из ключевых преимуществ fetchBaseQuery — автоматическая работа с JSON.

Пример:

body: {
    name: 'Alex',
}

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

  1. вызывает JSON.stringify;

  2. устанавливает:

    Content-Type: application/json
  3. преобразует ответ через response.json().


Автоматический парсинг ответа

Если сервер возвращает:

{
    "id": 1,
    "name": "John"
}

то endpoint получает уже готовый объект:

data.id
data.name

Без ручного:

await response.json()

GET-запросы

Простейший пример

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

Запрос по ID

getPost: builder.query({
    query: (id) => `/posts/${id}`,
})

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

const { data } = useGetPostQuery(5)

Query-параметры

getPosts: builder.query({
    query: ({ page, lim it }) => ({
        url: '/posts',
        params: {
            page,
            lim it,
        },
    }),
})

URL:

/posts?page=1&limit=20

POST-запросы

Создание записи

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

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

const [createPost] = useCreatePostMutation()

await createPost({
    title: 'Article',
})

PUT-запросы

Полное обновление сущности:

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

PATCH-запросы

Частичное обновление:

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

DELETE-запросы

Удаление сущности:

deletePost: builder.mutation({
    query: (id) => ({
        url: `/posts/${id}`,
        method: 'DELETE',
    }),
})

prepareHeaders

Назначение

prepareHeaders используется для:

  • добавления токенов;
  • авторизации;
  • глобальных заголовков;
  • модификации request headers.

Базовый пример

fetchBaseQuery({
    baseUrl: '/api',
    prepareHeaders: (headers) => {
        headers.set('X-App', 'RTK')

        return headers
    },
})

JWT-авторизация

Самый распространённый сценарий:

fetchBaseQuery({
    baseUrl: '/api',
    prepareHeaders: (headers, { getState }) => {
        const token = getState().auth.token

        if (token) {
            headers.set('Authorization', `Bearer ${token}`)
        }

        return headers
    },
})

Второй аргумент prepareHeaders

RTK Query передаёт:

{
    getState,
    endpoint,
    type,
    forced,
}

Пример:

prepareHeaders: (headers, api) => {
    console.log(api.endpoint)

    return headers
}

credentials

Используется для cookie-based авторизации.

Варианты

credentials: 'include'
credentials: 'same-origin'
credentials: 'omit'

fetchBaseQuery({
    baseUrl: '/api',
    credentials: 'include',
})

timeout

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

fetchBaseQuery({
    baseUrl: '/api',
    timeout: 5000,
})

Если сервер не ответит за 5 секунд — запрос завершится ошибкой.


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

Формат ошибки

RTK Query возвращает объект:

{
    error: {
        status,
        data,
    }
}

Проверка ошибок

const { error } = useGetPostsQuery()

if (error) {
    console.log(error.status)
}

HTTP-статусы

404

{
    status: 404,
    data: {...}
}

500

{
    status: 500,
    data: {...}
}

Ошибки сети

Если сервер недоступен:

{
    status: 'FETCH_ERROR',
    error: 'TypeError: Failed to fetch'
}

Ошибки парсинга

{
    status: 'PARSING_ERROR',
}

Причина — сервер вернул невалидный JSON.


Ошибки таймаута

{
    status: 'TIMEOUT_ERROR',
}

responseHandler

Позволяет управлять обработкой ответа.

JSON по умолчанию

Стандартное поведение:

response.json()

Получение текста

query: () => ({
    url: '/text',
    responseHandler: 'text',
})

Получение blob

query: () => ({
    url: '/file',
    responseHandler: (response) => response.blob(),
})

Загрузка файлов

Скачать PDF

downloadFile: builder.query({
    query: () => ({
        url: '/report',
        responseHandler: (response) => response.blob(),
    }),
})

validateStatus

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


Пример

query: () => ({
    url: '/posts',
    validateStatus: (response, result) => {
        return response.status === 200
    },
})

Нестандартные API

Некоторые серверы всегда возвращают 200.

Ошибка хранится внутри JSON:

{
    "success": false
}

Тогда:

validateStatus: (response, result) => {
    return result.success
}

paramsSerializer

Назначение

Настройка сериализации query-параметров.


Стандартное поведение

params: {
    tags: ['js', 'react'],
}

Может превратиться в:

tags=js&tags=react

Кастомный сериализатор

fetchBaseQuery({
    baseUrl: '/api',
    paramsSerializer: (params) => {
        return new URLSearchParams(params).toString()
    },
})

fetchFn

Замена стандартного fetch

Иногда необходимо:

  • SSR;
  • Node.js;
  • тестирование;
  • polyfill;
  • кастомный HTTP-клиент.

Пример

fetchBaseQuery({
    baseUrl: '/api',
    fetchFn: customFetch,
})

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

Например, в Next.js:

import fetch from 'cross-fetch'

fetchBaseQuery({
    baseUrl: 'https://api.site.com',
    fetchFn: fetch,
})

Комбинирование с reauth

Одна из самых популярных архитектур — автоматическое обновление access token.


Базовый baseQuery

const baseQuery = fetchBaseQuery({
    baseUrl: '/api',
    prepareHeaders: (headers, { getState }) => {
        const token = getState().auth.token

        if (token) {
            headers.set('Authorization', `Bearer ${token}`)
        }

        return headers
    },
})

Обёртка над fetchBaseQuery

const baseQueryWithReauth = async (args, api, extraOptions) => {
    let result = await baseQuery(args, api, extraOptions)

    if (result.error?.status === 401) {
        const refreshResult = await baseQuery(
            {
                url: '/refresh',
                method: 'POST',
            },
            api,
            extraOptions
        )

        if (refreshResult.data) {
            api.dispatch(setToken(refreshResult.data.token))

            result = await baseQuery(args, api, extraOptions)
        }
    }

    return result
}

Использование обёртки

export const api = createApi({
    reducerPath: 'api',
    baseQuery: baseQueryWithReauth,
    endpoints: () => ({}),
})

Работа с FormData

Отправка файлов

uploadAvatar: builder.mutation({
    query: (file) => {
        const formData = new FormData()

        formData.append('avatar', file)

        return {
            url: '/upload',
            method: 'POST',
            body: formData,
        }
    },
})

Важная особенность FormData

Нельзя вручную устанавливать:

Content-Type: multipart/form-data

Браузер сам добавляет boundary.


meta в ответе

RTK Query может вернуть metadata:

const result = await baseQuery(args, api, extraOptions)

console.log(result.meta)

Содержимое meta

Обычно:

{
    request,
    response,
}

Доступ к response headers

transformResponse: (response, meta) => {
    console.log(meta.response.headers.get('X-Total-Count'))

    return response
}

transformResponse

Позволяет изменить данные до попадания в cache.


Пример преобразования

Сервер:

{
    "data": {
        "items": []
    }
}

Endpoint:

getPosts: builder.query({
    query: () => '/posts',
    transformResponse: (response) => {
        return response.data.items
    },
})

transformErrorResponse

Позволяет нормализовать ошибки.

transformErrorResponse: (response) => {
    return response.data.message
}

Отличие от Axios

fetchBaseQuery легче

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

  • встроен в RTK Query;
  • меньше размер bundle;
  • не требует дополнительной библиотеки;
  • основан на стандартном fetch.

Axios предоставляет больше возможностей

Например:

  • interceptors;
  • автоматические retry;
  • отмена старых API;
  • расширенная сериализация.

Когда fetchBaseQuery достаточно

Подходит для:

  • REST API;
  • CRUD;
  • JWT;
  • SPA;
  • стандартных HTTP-запросов;
  • большинства frontend-приложений.

Когда нужен кастомный baseQuery

Иногда fetchBaseQuery недостаточно.

Например:

  • GraphQL;
  • gRPC;
  • websocket-гибриды;
  • сложные retry-стратегии;
  • централизованные interceptors;
  • нестандартная авторизация.

Практическая архитектура API-сервиса

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

const baseQuery = fetchBaseQuery({
    baseUrl: 'https://api.site.com',
    credentials: 'include',

    prepareHeaders: (headers, { getState }) => {
        const token = getState().auth.token

        if (token) {
            headers.set(
                'Authorization',
                `Bearer ${token}`
            )
        }

        return headers
    },
})

export const api = createApi({
    reducerPath: 'api',

    baseQuery,

    tagTypes: ['Posts', 'Users'],

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

        getPost: builder.query({
            query: (id) => `/posts/${id}`,
        }),

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

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

        deletePost: builder.mutation({
            query: (id) => ({
                url: `/posts/${id}`,
                method: 'DELETE',
            }),
        }),
    }),
})