Endpoints и их типы

Endpoint — это описание конкретной операции взаимодействия с сервером внутри API-слайса, созданного через createApi. Каждый endpoint отвечает за один тип действия:

  • получение данных;
  • изменение данных;
  • отправку формы;
  • удаление сущности;
  • выполнение произвольного HTTP-запроса.

В RTK Query endpoints объявляются внутри свойства endpoints:

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

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

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

Функция endpoints принимает объект builder, через который создаются endpoint-описания.

RTK Query поддерживает два основных типа endpoint:

  • query
  • mutation

Каждый тип имеет собственное назначение, особенности кэширования и поведение.


Тип endpoint: query

query используется для получения данных.

Основная задача query-endpoint — загрузка информации с сервера и хранение результата в кэше RTK Query.

Пример:

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

Такой endpoint автоматически создаёт React hook:

const { data, isLoading } = useGetPostsQuery()

Особенности query endpoint

Автоматическое кэширование

RTK Query сохраняет результаты запросов в store Redux.

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

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

Повторный вызов:

useGetUserQuery(5)

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


Автоматическое управление состоянием

RTK Query автоматически создаёт состояния:

const {
    data,
    error,
    isLoading,
    isFetching,
    isSuccess,
    isError
} = useGetUsersQuery()

Каждое поле отражает текущее состояние запроса.


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

Query-endpoints поддерживают автоматические повторные обновления данных:

useGetPostsQuery(undefined, {
    pollingInterval: 5000
})

RTK Query будет обновлять данные каждые 5 секунд.


Refetch при фокусе окна

useGetPostsQuery(undefined, {
    refetchOnFocus: true
})

При возврате во вкладку браузера данные обновятся автоматически.


Refetch при восстановлении сети

useGetPostsQuery(undefined, {
    refetchOnReconnect: true
})

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


Сигнатура builder.query

Полная структура:

builder.query({
    query,
    transformResponse,
    transformErrorResponse,
    providesTags,
    keepUnusedDataFor,
    serializeQueryArgs,
    merge,
    forceRefetch,
    onQueryStarted,
    onCacheEntryAdded
})

Каждое свойство отвечает за отдельный аспект поведения endpoint.


Свойство query

query описывает параметры HTTP-запроса.

Простой вариант

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

Query с параметрами

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

Объект конфигурации запроса

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

Можно указывать:

  • url
  • method
  • body
  • params
  • headers

Query arguments

Аргументы query участвуют в формировании ключа кэша.

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

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

useGetPostsQuery({
    page: 1,
    limit: 20
})

RTK Query сериализует аргументы и создаёт уникальный cache key.


transformResponse

Позволяет преобразовывать ответ сервера до сохранения в store.

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

    transformResponse: (response) => {
        return response.data
    }
})

Полезно при работе с API следующего вида:

{
    "success": true,
    "data": []
}

transformErrorResponse

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

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

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

providesTags

Используется для связывания query с системой инвалидации кэша.

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

    providesTags: ['Posts']
})

После мутации RTK Query сможет автоматически обновить этот query.


keepUnusedDataFor

Указывает время хранения кэша после удаления последнего подписчика.

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

    keepUnusedDataFor: 60
})

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


serializeQueryArgs

Позволяет кастомизировать формирование cache key.

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

    serializeQueryArgs: ({ endpointName }) => {
        return endpointName
    }
})

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


merge

Используется для объединения старого и нового кэша.

Особенно полезно при пагинации.

getPosts: builder.query({
    query: (page) => `/posts?page=${page}`,

    serializeQueryArgs: ({ endpointName }) => endpointName,

    merge: (currentCache, newItems) => {
        currentCache.push(...newItems)
    }
})

forceRefetch

Определяет необходимость принудительного обновления.

forceRefetch({ currentArg, previousArg }) {
    return currentArg !== previousArg
}

onQueryStarted

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

Часто используется для optimistic update.

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)

                    if (post) {
                        Object.assign(post, arg)
                    }
                }
            )
        )

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

onCacheEntryAdded

Позволяет реагировать на создание и удаление кэша.

Часто используется для WebSocket.

getNotifications: builder.query({
    query: () => '/notifications',

    async onCacheEntryAdded(
        arg,
        {
            updateCachedData,
            cacheDataLoaded,
            cacheEntryRemoved
        }
    ) {
        await cacheDataLoaded

        const socket = new WebSocket('ws://localhost:3000')

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

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

        await cacheEntryRemoved

        socket.close()
    }
})

Тип endpoint: mutation

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

Примеры:

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

Пример mutation endpoint

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

RTK Query создаст hook:

const [createPost, result] = useCreatePostMutation()

Структура mutation hook

const [
    createPost,
    {
        data,
        error,
        isLoading,
        isSuccess,
        isError
    }
] = useCreatePostMutation()

Вызов mutation

await createPost({
    title: 'New post'
})

unwrap

Метод unwrap позволяет получить чистый результат или выбросить ошибку.

try {
    const result = await createPost(data).unwrap()

    console.log(result)
} catch (error) {
    console.error(error)
}

Без unwrap mutation всегда возвращает action object.


invalidatesTags

Mutation обычно инвалидирует query-кэш.

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

    invalidatesTags: ['Posts']
})

После успешной мутации RTK Query автоматически перезапросит все query с providesTags: ['Posts'].


Разница между providesTags и invalidatesTags

Query

providesTags: ['Posts']

Endpoint сообщает:

«Я предоставляю эти данные»


Mutation

invalidatesTags: ['Posts']

Endpoint сообщает:

«Эти данные устарели»


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

Теги могут зависеть от результата.

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

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

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

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

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

Query endpoint с POST-запросом

RTK Query не запрещает использовать POST внутри query.

searchPosts: builder.query({
    query: (filters) => ({
        url: '/posts/search',
        method: 'POST',
        body: filters
    })
})

Главный критерий:

  • query — получение данных;
  • mutation — изменение данных.

Разделение endpoint по доменам

Крупные API обычно группируют по сущностям.

endpoints: (builder) => ({
    getUsers: builder.query(...),
    getUser: builder.query(...),

    getPosts: builder.query(...),
    createPost: builder.mutation(...),

    getComments: builder.query(...)
})

Инъекция endpoints

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

const extendedApi = api.injectEndpoints({
    endpoints: (builder) => ({
        getProfile: builder.query({
            query: () => '/profile'
        })
    })
})

Особенно важно для:

  • code splitting;
  • lazy loading;
  • модульной архитектуры;
  • больших приложений.

Override существующих endpoints

api.injectEndpoints({
    overrideExisting: true,

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

Endpoint names

Имена endpoint имеют большое значение.

Из имени автоматически формируются:

  • hooks;
  • action creators;
  • selectors.
getUsers

Превращается в:

useGetUsersQuery

Mutation:

createPost

Превращается в:

useCreatePostMutation

Рекомендации по именованию

Для query

Используются префиксы:

  • get
  • fetch
  • load

Примеры:

getUsers
getUser
fetchPosts
loadProfile

Для mutation

Используются глаголы действия:

createPost
updatePost
deletePost
login
logout
uploadAvatar

Endpoint lifecycle

Каждый endpoint проходит через жизненный цикл:

  1. Инициализация
  2. Отправка запроса
  3. Получение ответа
  4. Обновление store
  5. Подписка компонентов
  6. Удаление кэша

RTK Query автоматизирует весь этот процесс.


Внутреннее хранение endpoints

RTK Query хранит:

  • аргументы запроса;
  • статус;
  • данные;
  • ошибки;
  • timestamp;
  • subscriptions;
  • cache lifetime.

Структура внутри Redux store:

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

Query vs Mutation

Характеристика Query Mutation
Назначение Получение данных Изменение данных
Кэширование Да Нет
Автоподписки Да Нет
Polling Да Нет
Refetch Да Ограниченно
Tags providesTags invalidatesTags
Хранение результата Долгое Кратковременное

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

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

  • списков;
  • карточек;
  • таблиц;
  • профилей;
  • dashboard;
  • поиска;
  • аналитики.

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

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

  • CRUD-операций;
  • отправки форм;
  • авторизации;
  • загрузки файлов;
  • действий пользователя;
  • серверных изменений.

Сложные endpoint-конфигурации

RTK Query позволяет комбинировать множество возможностей одновременно.

getFeed: builder.query({
    query: ({ page }) => ({
        url: '/feed',
        params: { page }
    }),

    serializeQueryArgs: ({ endpointName }) => endpointName,

    merge: (currentCache, newCache) => {
        currentCache.items.push(...newCache.items)
    },

    forceRefetch({ currentArg, previousArg }) {
        return currentArg?.page !== previousArg?.page
    },

    providesTags: ['Feed'],

    keepUnusedDataFor: 300
})

Такой endpoint реализует:

  • пагинацию;
  • единый кэш;
  • накопление данных;
  • автоматическую инвалидацию;
  • длительное хранение кэша.