Параметр baseQuery

Параметр baseQuery является фундаментальной частью API RTK Query. Именно через него выполняются HTTP-запросы, обрабатываются ответы сервера, формируются ошибки, добавляются заголовки, токены авторизации и реализуется любая кастомная логика взаимодействия с backend.

baseQuery указывается внутри функции createApi:

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

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

Без baseQuery RTK Query не знает, каким образом выполнять сетевые запросы.


Роль baseQuery в архитектуре RTK Query

baseQuery выполняет несколько важных задач:

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

Каждый endpoint внутри endpoints использует единый baseQuery, если явно не указано иное.

Архитектурно это выглядит следующим образом:

Component
   ↓
Hook RTK Query
   ↓
Endpoint
   ↓
baseQuery
   ↓
HTTP request
   ↓
Server

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

RTK Query поставляется со встроенным fetchBaseQuery.

Это небольшая обёртка над стандартным браузерным API fetch.

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

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

Пример:

baseQuery: fetchBaseQuery({
    baseUrl: 'https://jsonplaceholder.typicode.com',
})

После этого endpoint может использовать относительные URL:

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

Фактический адрес запроса:

https://jsonplaceholder.typicode.com/users

Параметр baseUrl

Наиболее распространённая настройка.

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

Позволяет избежать повторения одинаковых частей URL.

Без baseUrl:

query: () => 'https://api.site.com/posts'

С baseUrl:

query: () => '/posts'

Формат endpoint-запросов

Функция query может возвращать:

  • строку;
  • объект конфигурации.

Возврат строки

query: () => '/posts'

RTK Query автоматически выполнит GET-запрос.


Возврат объекта

query: () => ({
    url: '/posts',
    method: 'POST',
    body: {
        title: 'New post',
    },
})

Полная конфигурация:

query: () => ({
    url,
    method,
    body,
    params,
    headers,
    credentials,
    responseHandler,
})

HTTP-методы

Поддерживаются любые HTTP-методы.

GET

query: () => ({
    url: '/posts',
    method: 'GET',
})

POST

query: (data) => ({
    url: '/posts',
    method: 'POST',
    body: data,
})

PUT

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

PATCH

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

DELETE

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

Передача query-параметров

Параметр params автоматически сериализуется в query string.

query: () => ({
    url: '/posts',
    params: {
        page: 1,
        lim it: 10,
    },
})

Результат:

/posts?page=1&limit=10

Передача тела запроса

fetchBaseQuery автоматически сериализует объект в JSON.

query: (user) => ({
    url: '/users',
    method: 'POST',
    body: user,
})

Фактически:

JSON.stringify(user)

Также автоматически устанавливается заголовок:

Content-Type: application/json

Заголовки запроса

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

query: () => ({
    url: '/profile',
    headers: {
        Authorization: 'Bearer token',
    },
})

Глобальные заголовки через prepareHeaders

Наиболее важный механизм настройки baseQuery.

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

  • JWT;
  • access token;
  • language headers;
  • tenant headers;
  • API keys.

Пример:

baseQuery: fetchBaseQuery({
    baseUrl: 'https://api.site.com',

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

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

        return headers;
    },
})

Объект prepareHeaders

Второй аргумент содержит полезную информацию:

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

Доступны:

{
    getState,
    endpoint,
    type,
    forced,
    extra
}

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

Получение данных из Redux Store.

prepareHeaders: (headers, { getState }) => {
    const state = getState();

    headers.set('X-Language', state.settings.language);

    return headers;
}

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

Позволяет определять текущий endpoint.

prepareHeaders: (headers, { endpoint }) => {
    if (endpoint === 'uploadAvatar') {
        headers.set('X-Upload', 'true');
    }

    return headers;
}

Авторизация через Bearer Token

Наиболее распространённый сценарий.

baseQuery: fetchBaseQuery({
    baseUrl: '/api',

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

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

        return headers;
    },
})

По умолчанию fetch не отправляет cookie между доменами.

Для включения:

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

Возможные значения:

omit
same-origin
include

Обработка ответов

RTK Query автоматически пытается распарсить JSON.

Например:

{
    "id": 1,
    "title": "Post"
}

преобразуется в JavaScript-объект.


responseHandler

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

Текстовый ответ

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

Blob

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

Пользовательский обработчик

query: () => ({
    url: '/data',
    responseHandler: async (response) => {
        const text = await response.text();

        return text.toUpperCase();
    },
})

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

При ошибках RTK Query формирует объект:

{
    error: {
        status,
        data
    }
}

Пример:

{
    status: 404,
    data: {
        message: 'Not found'
    }
}

Типы ошибок

HTTP ошибки

400
401
403
404
500

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

{
    status: 'PARSING_ERROR'
}

Ошибки сети

{
    status: 'FETCH_ERROR'
}

Timeout

{
    status: 'TIMEOUT_ERROR'
}

Timeout запросов

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

Если сервер не ответил за 5 секунд:

{
    error: {
        status: 'TIMEOUT_ERROR'
    }
}

Пользовательский baseQuery

RTK Query позволяет полностью заменить fetchBaseQuery.

Сигнатура:

const customBaseQuery = async (
    args,
    api,
    extraOptions
) => {
    return {
        data,
    };

    // или

    return {
        error,
    };
};

Аргументы custom baseQuery

args

Аргументы запроса.

api

Служебные методы RTK Query.

extraOptions

Дополнительные параметры.


Простейший custom baseQuery

const customBaseQuery = async (args) => {
    try {
        const response = await fetch(args.url);

        const data = await response.json();

        return { data };
    } catch (error) {
        return {
            error: {
                status: 'CUSTOM_ERROR',
                data: error,
            },
        };
    }
};

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

RTK Query не зависит от fetch.

Можно использовать Axios.

import axios from 'axios';

const axiosBaseQuery =
    ({ baseUrl }) =>
    async ({ url, method, data, params }) => {
        try {
            const result = await axios({
                url: baseUrl + url,
                method,
                data,
                params,
            });

            return {
                data: result.data,
            };
        } catch (axiosError) {
            return {
                error: {
                    status: axiosError.response?.status,
                    data: axiosError.response?.data,
                },
            };
        }
    };

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

baseQuery: axiosBaseQuery({
    baseUrl: '/api',
})

Повторная авторизация через refresh token

Один из самых важных сценариев custom baseQuery.

Схема:

  1. access token истёк;
  2. сервер возвращает 401;
  3. выполняется refresh token запрос;
  4. access token обновляется;
  5. исходный запрос повторяется.

Реализация refresh token

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

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

        return headers;
    },
});

Обёртка над baseQuery

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(
                setCredentials(refreshResult.data)
            );

            result = await baseQuery(
                args,
                api,
                extraOptions
            );
        } else {
            api.dispatch(logout());
        }
    }

    return result;
};

Использование dispatch внутри baseQuery

api предоставляет доступ к Redux dispatch.

api.dispatch(action())

Пример:

api.dispatch(logout())

Использование getState внутри custom baseQuery

const state = api.getState();

Пример:

const locale = api.getState().settings.locale;

Логирование запросов

const loggingBaseQuery = async (
    args,
    api,
    extraOptions
) => {
    console.log('Request:', args);

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

    console.log('Response:', result);

    return result;
};

Глобальная обработка ошибок

const baseQueryWithErrors = async (
    args,
    api,
    extraOptions
) => {
    const result = await baseQuery(
        args,
        api,
        extraOptions
    );

    if (result.error?.status === 500) {
        api.dispatch(showServerError());
    }

    return result;
};

Добавление динамических параметров

const dynamicBaseQuery = async (
    args,
    api,
    extraOptions
) => {
    const state = api.getState();

    const tenantId = state.auth.tenantId;

    args.params = {
        ...args.params,
        tenantId,
    };

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

Интеграция с GraphQL

RTK Query может использовать GraphQL вместо REST.

import { request } from 'graphql-request';

const graphqlBaseQuery =
    ({ baseUrl }) =>
    async ({ body }) => {
        try {
            const result = await request(
                baseUrl,
                body
            );

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

Использование queryFn вместо baseQuery

Иногда endpoint полностью переопределяет механизм запроса.

getUser: builder.query({
    async queryFn(id) {
        try {
            const response = await fetch(`/users/${id}`);
            const data = await response.json();

            return { data };
        } catch (error) {
            return { error };
        }
    },
})

В этом случае baseQuery не используется.


Различие между query и baseQuery

query

Описывает конкретный endpoint.

query: (id) => `/users/${id}`

baseQuery

Определяет механизм выполнения всех запросов.

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

Каскад custom baseQuery

Можно создавать цепочки.

baseQuery
→ auth wrapper
→ logging wrapper
→ retry wrapper
→ final request

Retry-механизм

RTK Query содержит встроенный retry wrapper.

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

Пример:

const staggeredBaseQuery = retry(
    fetchBaseQuery({
        baseUrl: '/api',
    }),
    {
        maxRetries: 5,
    }
);

Прерывание запросов

baseQuery поддерживает AbortController.

const customBaseQuery = async (
    args,
    api
) => {
    const response = await fetch(args.url, {
        signal: api.signal,
    });

    return {
        data: await response.json(),
    };
};

Доступ к endpoint name

const customBaseQuery = async (
    args,
    api
) => {
    console.log(api.endpoint);
};

Доступ к типу запроса

console.log(api.type);

Значения:

query
mutation

extraOptions

Дополнительные параметры endpoint.

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

Получение:

const customBaseQuery = async (
    args,
    api,
    extraOptions
) => {
    console.log(extraOptions.retry);
};

Полная структура createApi с baseQuery

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

    baseQuery: fetchBaseQuery({
        baseUrl: '/api',

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

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

            return headers;
        },
    }),

    tagTypes: ['Posts'],

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

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

Практические сценарии использования baseQuery

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

prepareHeaders

Refresh token

baseQueryWithReauth

API Gateway

baseUrl

Мультитенантность

tenant headers

SSR

custom fetch implementation

GraphQL

graphqlBaseQuery

Axios integration

axiosBaseQuery

Retry логика

retry(fetchBaseQuery())

Типичные ошибки при работе с baseQuery

Отсутствие return headers

Неправильно:

prepareHeaders: (headers) => {
    headers.set('Authorization', 'token');
}

Правильно:

prepareHeaders: (headers) => {
    headers.set('Authorization', 'token');

    return headers;
}

Неправильный формат ошибки

Неправильно:

return {
    message: 'error'
}

Правильно:

return {
    error: {
        status: 500,
        data: 'error',
    },
}

Исключение вместо error

Неправильно:

throw error;

Правильно:

return {
    error,
};

Производительность baseQuery

fetchBaseQuery специально реализован минималистично:

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

По этой причине fetchBaseQuery рекомендуется использовать по умолчанию, если проект не требует сложной HTTP-инфраструктуры.