Функция createApi

Функция createApi является центральным элементом библиотеки RTK Query. Именно она формирует API-слой приложения, генерирует endpoints, создает middleware, reducer, React hooks и управляет системой кэширования.

Внутри RTK Query практически вся архитектура строится вокруг одного или нескольких экземпляров API, созданных через createApi.

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

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

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

После выполнения createApi создается объект API, содержащий:

api.reducer
api.middleware
api.endpoints
api.util
api.injectEndpoints

При использовании React автоматически генерируются hooks:

api.useGetPostsQuery

Общая архитектура createApi

Функция принимает конфигурационный объект:

createApi({
    reducerPath,
    baseQuery,
    tagTypes,
    endpoints,
    keepUnusedDataFor,
    refetchOnFocus,
    refetchOnReconnect,
    refetchOnMountOrArgChange,
    extractRehydrationInfo,
    serializeQueryArgs,
    invalidationBehavior
})

Каждое поле влияет на поведение всей системы API.


Поле reducerPath

Назначение

reducerPath определяет имя раздела store, в котором RTK Query хранит свое состояние.

Пример:

reducerPath: 'api'

В Redux Store появится раздел:

state.api

Внутри будут храниться:

  • кэш запросов;
  • статусы загрузки;
  • subscriptions;
  • mutation state;
  • metadata;
  • timestamps;
  • tags.

Пример структуры state

{
    api: {
        queries: {},
        mutations: {},
        provided: {},
        subscriptions: {},
        config: {}
    }
}

Несколько API

Можно создавать несколько API:

export const usersApi = createApi({
    reducerPath: 'usersApi',
    ...
})

export const postsApi = createApi({
    reducerPath: 'postsApi',
    ...
})

Тогда store будет содержать:

{
    usersApi: {},
    postsApi: {}
}

Важность уникальности

Каждый reducerPath обязан быть уникальным.

Ошибка:

createApi({
    reducerPath: 'api'
})

createApi({
    reducerPath: 'api'
})

Это приведет к конфликту reducers и middleware.


Поле baseQuery

Назначение

baseQuery — базовый механизм выполнения HTTP-запросов.

RTK Query вызывает его для каждого endpoint.


fetchBaseQuery

Наиболее распространенный вариант:

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

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

Это обертка над fetch.


Как работает fetchBaseQuery

При запросе:

query: () => 'users'

будет выполнено:

fetch('/api/users')

Полная настройка

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

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

        return headers
    }
})

Параметр prepareHeaders

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

  • JWT;
  • access token;
  • language headers;
  • tenant headers;
  • custom authorization.

Пример с локализацией

prepareHeaders: (headers, { getState }) => {
    const lang = getState().settings.language

    headers.set('Accept-Language', lang)

    return headers
}

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

Вместо fetchBaseQuery можно написать собственную функцию.

Пример:

const customBaseQuery = async (args, api, extraOptions) => {
    try {
        const response = await axios(args)

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

Сигнатура baseQuery

const baseQuery = async (
    args,
    api,
    extraOptions
) => {}

Параметр args

Содержит параметры запроса:

{
    url,
    method,
    body,
    params
}

Параметр api

Содержит:

api.dispatch
api.getState
api.signal
api.abort
api.endpoint
api.type

Параметр signal

Используется для отмены запросов:

const response = await fetch(url, {
    signal: api.signal
})

Параметр extraOptions

Позволяет передавать дополнительные настройки endpoint.


Поле tagTypes

Назначение

tagTypes описывает список тегов, используемых системой инвалидации кэша.

Пример:

tagTypes: ['Post', 'User']

Как работают теги

RTK Query связывает:

  • query endpoints;
  • mutation endpoints;
  • кэшированные данные.

Через:

providesTags
invalidatesTags

Пример

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

Mutation:

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

После mutation RTK Query автоматически перезапросит getPosts.


Теги с ID

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

Инвалидация конкретной записи

invalidatesTags: (result, error, id) => [
    { type: 'Post', id }
]

Поле endpoints

Назначение

endpoints описывает все API endpoints.

Пример:

endpoints: (builder) => ({
    getPosts: builder.query({}),
    addPost: builder.mutation({})
})

Объект builder

Builder предоставляет:

builder.query()
builder.mutation()

builder.query

Используется для GET-запросов и операций чтения.

Пример:

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

builder.mutation

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

  • POST;
  • PUT;
  • PATCH;
  • DELETE.

Пример:

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

Поле keepUnusedDataFor

Назначение

Определяет время хранения кэша после отписки компонентов.


Пример

keepUnusedDataFor: 60

Данные будут храниться 60 секунд.


Механизм работы

Когда последний компонент отписывается:

const { data } = useGetPostsQuery()

RTK Query запускает таймер удаления.


Повторное подключение

Если компонент снова подпишется:

useGetPostsQuery()

до удаления кэша, повторного запроса не произойдет.


Поле refetchOnFocus

Назначение

Автоматический refetch при возвращении вкладки в фокус.


Пример

refetchOnFocus: true

Как это работает

При переключении вкладок:

  1. пользователь уходит со страницы;
  2. данные устаревают;
  3. пользователь возвращается;
  4. RTK Query делает refetch.

Поле refetchOnReconnect

Назначение

Повторный запрос после восстановления интернета.


Пример

refetchOnReconnect: true

Поле refetchOnMountOrArgChange

Назначение

Управляет refetch при:

  • mount;
  • изменении аргументов.

Вариант boolean

refetchOnMountOrArgChange: true

Всегда выполнять повторный запрос.


Вариант number

refetchOnMountOrArgChange: 30

Повторный запрос только если кэш старше 30 секунд.


Поле extractRehydrationInfo

Назначение

Используется для SSR и hydration.

Особенно важно в:

  • Next.js;
  • server rendering;
  • persistence.

Пример

extractRehydrationInfo(action, { reducerPath }) {
    if (action.type === HYDRATE) {
        return action.payload[reducerPath]
    }
}

Поле serializeQueryArgs

Назначение

Управляет генерацией cache key.


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

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

useGetUserQuery(5)

Ключ:

getUser(5)

Пользовательская сериализация

serializeQueryArgs: ({
    endpointName,
    queryArgs
}) => {
    return `${endpointName}-${queryArgs.id}`
}

Практическое применение

Полезно для:

  • pagination;
  • infinite scroll;
  • сложных фильтров;
  • нормализации cache keys.

Поле invalidationBehavior

Назначение

Определяет момент инвалидации тегов.


Значение delayed

Поведение по умолчанию.

invalidationBehavior: 'delayed'

Инвалидация откладывается до завершения всех запросов.


Значение immediately

invalidationBehavior: 'immediately'

Инвалидация выполняется сразу.


Генерация hooks

RTK Query автоматически создает React hooks.


Query hooks

const {
    data,
    error,
    isLoading
} = useGetPostsQuery()

Mutation hooks

const [
    addPost,
    result
] = useAddPostMutation()

Lazy hooks

const [
    trigger,
    result
] = useLazyGetPostsQuery()

Объект api.endpoints

После создания API появляется объект endpoints:

api.endpoints.getPosts

Возможности

api.endpoints.getPosts.initiate()
api.endpoints.getPosts.select()

Ручной запуск запросов

initiate

dispatch(
    api.endpoints.getPosts.initiate()
)

Практическое применение

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

  • вне React;
  • в thunk;
  • в middleware;
  • в SSR.

Middleware RTK Query

createApi создает middleware:

api.middleware

Назначение middleware

Middleware управляет:

  • lifecycle запросов;
  • polling;
  • cache cleanup;
  • subscriptions;
  • refetching;
  • invalidation;
  • reconnect handling.

Reducer RTK Query

createApi генерирует reducer:

api.reducer

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

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

Внутренний lifecycle запросов

Этапы выполнения

RTK Query проходит несколько стадий:

  1. subscription;
  2. request start;
  3. loading state;
  4. response processing;
  5. cache update;
  6. notification subscribers.

Cache key

Каждый запрос получает уникальный ключ.


Пример

useGetUserQuery(1)

Ключ:

getUser(1)

Одинаковые запросы

useGetUserQuery(1)
useGetUserQuery(1)

Используют один cache entry.


Deduplication

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


Пример

Три компонента:

useGetPostsQuery()

Выполнят только один HTTP-запрос.


Polling

createApi поддерживает polling.


Пример

useGetPostsQuery(undefined, {
    pollingInterval: 5000
})

Запрос каждые 5 секунд.


Streaming updates

RTK Query поддерживает WebSocket и SSE через lifecycle API.


Пример

async onCacheEntryAdded(
    arg,
    {
        updateCachedData,
        cacheDataLoaded,
        cacheEntryRemoved
    }
) {
    const ws = new WebSocket('ws://localhost')

    try {
        await cacheDataLoaded

        ws.onmess age = (event) => {
            const data = JSON.parse(event.data)

            updateCachedData((draft) => {
                draft.push(data)
            })
        }

        await cacheEntryRemoved

        ws.close()
    } catch {
        ws.close()
    }
}

Code splitting

RTK Query поддерживает динамическое расширение API.


injectEndpoints

const extendedApi = api.injectEndpoints({
    endpoints: (builder) => ({
        getComments: builder.query({
            query: () => 'comments'
        })
    })
})

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

Позволяет:

  • разделять API по модулям;
  • загружать endpoints лениво;
  • уменьшать bundle size.

Утилиты api.util

createApi генерирует набор utilities.


invalidateTags

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

resetApiState

dispatch(api.util.resetApiState())

Полностью очищает кэш.


updateQueryData

dispatch(
    api.util.updateQueryData(
        'getPosts',
        undefined,
        (draft) => {
            draft.push(newPost)
        }
    )
)

Optimistic updates

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


Пример

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

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

                    Object.assign(post, arg)
                }
            )
        )

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

Ошибки в createApi

Отсутствие middleware

Ошибка:

middleware: []

Без:

api.middleware

RTK Query работать не будет.


Отсутствие reducer

Ошибка:

reducer: {}

Без:

[api.reducerPath]: api.reducer

кэширование и state management не работают.


Повторяющийся reducerPath

Вызывает конфликты store.


Отсутствие tagTypes

Инвалидация может работать некорректно.


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

Крупные приложения обычно разделяют API:

/api
    baseApi.js
    usersApi.js
    postsApi.js
    commentsApi.js

Базовый API

export const baseApi = createApi({
    reducerPath: 'api',
    baseQuery: fetchBaseQuery({
        baseUrl: '/api'
    }),
    endpoints: () => ({})
})

Расширение API

export const usersApi = baseApi.injectEndpoints({
    endpoints: (builder) => ({
        getUsers: builder.query({
            query: () => 'users'
        })
    })
})

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

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

  • количества запросов;
  • числа перерисовок;
  • объема store updates;
  • сетевой нагрузки.

Внутренние механизмы оптимизации

RTK Query использует:

  • request deduplication;
  • normalized subscriptions;
  • reference counting;
  • automatic batching;
  • intelligent cache reuse.

Когда использовать несколько API

Несколько createApi имеют смысл при:

  • полностью разных backend;
  • микрофронтендах;
  • независимых lifecycle;
  • изолированных middleware.

Когда использовать один API

Один createApi предпочтительнее при:

  • общем backend;
  • единой системе тегов;
  • shared cache;
  • cross-invalidation.

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

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

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: ['Post', 'User'],

    keepUnusedDataFor: 60,

    refetchOnFocus: true,

    refetchOnReconnect: true,

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

            providesTags: ['Post']
        }),

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

            providesTags: (result, error, id) => [
                { type: 'User', id }
            ]
        }),

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

            invalidatesTags: ['Post']
        }),

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

            invalidatesTags: (result, error, arg) => [
                {
                    type: 'User',
                    id: arg.id
                }
            ]
        })
    })
})