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;Полный адрес запроса:
https://jsonplaceholder.typicode.com/posts
fetchBaseQueryfetchBaseQuery представляет собой фабрику функций.
Вызов:
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;Минимальный вариант:
fetchBaseQuery({
baseUrl: 'https://api.example.com',
})
Чаще используется расширенная конфигурация:
fetchBaseQuery({
baseUrl: 'https://api.example.com',
prepareHeaders,
fetchFn,
paramsSerializer,
timeout,
credentials,
mode,
cache,
redirect,
referrerPolicy,
})
baseUrlbaseUrl задаёт префикс для всех запросов.
Пример:
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'
methodHTTP-метод:
method: 'POST'
Поддерживаются:
bodyТело запроса:
body: {
title: 'New post',
}
paramsQuery-параметры 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',
},
}),
})
Одно из ключевых преимуществ fetchBaseQuery —
автоматическая работа с JSON.
Пример:
body: {
name: 'Alex',
}
RTK Query автоматически:
вызывает JSON.stringify;
устанавливает:
Content-Type: application/jsonпреобразует ответ через response.json().
Если сервер возвращает:
{
"id": 1,
"name": "John"
}
то endpoint получает уже готовый объект:
data.id
data.name
Без ручного:
await response.json()
getPosts: builder.query({
query: () => '/posts',
})
getPost: builder.query({
query: (id) => `/posts/${id}`,
})
Использование:
const { data } = useGetPostQuery(5)
getPosts: builder.query({
query: ({ page, lim it }) => ({
url: '/posts',
params: {
page,
lim it,
},
}),
})
URL:
/posts?page=1&limit=20
createPost: builder.mutation({
query: (post) => ({
url: '/posts',
method: 'POST',
body: post,
}),
})
const [createPost] = useCreatePostMutation()
await createPost({
title: 'Article',
})
Полное обновление сущности:
updatePost: builder.mutation({
query: ({ id, ...body }) => ({
url: `/posts/${id}`,
method: 'PUT',
body,
}),
})
Частичное обновление:
patchPost: builder.mutation({
query: ({ id, ...body }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body,
}),
})
Удаление сущности:
deletePost: builder.mutation({
query: (id) => ({
url: `/posts/${id}`,
method: 'DELETE',
}),
})
prepareHeadersprepareHeaders используется для:
fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers) => {
headers.set('X-App', 'RTK')
return headers
},
})
Самый распространённый сценарий:
fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
if (token) {
headers.set('Authorization', `Bearer ${token}`)
}
return headers
},
})
prepareHeadersRTK 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)
}
{
status: 404,
data: {...}
}
{
status: 500,
data: {...}
}
Если сервер недоступен:
{
status: 'FETCH_ERROR',
error: 'TypeError: Failed to fetch'
}
{
status: 'PARSING_ERROR',
}
Причина — сервер вернул невалидный JSON.
{
status: 'TIMEOUT_ERROR',
}
responseHandlerПозволяет управлять обработкой ответа.
Стандартное поведение:
response.json()
query: () => ({
url: '/text',
responseHandler: 'text',
})
query: () => ({
url: '/file',
responseHandler: (response) => response.blob(),
})
downloadFile: builder.query({
query: () => ({
url: '/report',
responseHandler: (response) => response.blob(),
}),
})
validateStatusПозволяет самостоятельно определить успешность ответа.
query: () => ({
url: '/posts',
validateStatus: (response, result) => {
return response.status === 200
},
})
Некоторые серверы всегда возвращают 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Иногда необходимо:
fetchBaseQuery({
baseUrl: '/api',
fetchFn: customFetch,
})
Например, в Next.js:
import fetch from 'cross-fetch'
fetchBaseQuery({
baseUrl: 'https://api.site.com',
fetchFn: fetch,
})
Одна из самых популярных архитектур — автоматическое обновление access token.
baseQueryconst baseQuery = fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
if (token) {
headers.set('Authorization', `Bearer ${token}`)
}
return headers
},
})
fetchBaseQueryconst 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: () => ({}),
})
uploadAvatar: builder.mutation({
query: (file) => {
const formData = new FormData()
formData.append('avatar', file)
return {
url: '/upload',
method: 'POST',
body: 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,
}
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
}
fetchBaseQuery легчеПреимущества:
Например:
fetchBaseQuery достаточноПодходит для:
baseQueryИногда fetchBaseQuery недостаточно.
Например:
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',
}),
}),
}),
})