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

RTK Query редко используется полностью изолированно. В большинстве приложений библиотека становится частью более крупной архитектуры, включающей:

  • Redux Toolkit;
  • React Router;
  • Form-библиотеки;
  • серверные фреймворки;
  • WebSocket-инфраструктуру;
  • системы авторизации;
  • библиотеки кэширования;
  • SSR и SSG решения;
  • GraphQL-клиенты;
  • инструменты тестирования;
  • state management библиотеки.

Главная особенность RTK Query заключается в том, что он отвечает исключительно за слой работы с удалёнными данными. Это позволяет комбинировать его практически с любыми решениями без конфликтов архитектуры.


Совместное использование с классическим Redux

RTK Query является частью экосистемы Redux Toolkit, поэтому интеграция с обычными slice выполняется естественным образом.

Разделение ответственности

Типичная схема:

Задача Инструмент
Серверные данные RTK Query
UI state Redux slice
Модальные окна Redux slice
Фильтры интерфейса Redux slice
Кэш API RTK Query
Локальные формы Redux slice / form library

Комбинация api.reducer и обычных reducers

import { configureStore } from '@reduxjs/toolkit'
import { api } from './services/api'
import authReducer from './features/auth/authSlice'
import uiReducer from './features/ui/uiSlice'

export const store = configureStore({
    reducer: {
        [api.reducerPath]: api.reducer,
        auth: authReducer,
        ui: uiReducer
    },
    middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware().concat(api.middleware)
})

RTK Query не заменяет Redux store, а становится его специализированной частью.


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

RTK Query предоставляет доступ к Redux dispatch через lifecycle API.

getProfile: builder.query({
    query: () => '/profile',

    async onQueryStarted(arg, { dispatch, queryFulfilled }) {
        try {
            const { data } = await queryFulfilled

            dispatch(setProfileLoaded(true))
            dispatch(setUser(data))
        } catch (error) {
            dispatch(setProfileError(true))
        }
    }
})

Такой подход позволяет:

  • синхронизировать UI;
  • запускать дополнительные side effects;
  • обновлять unrelated state;
  • инициировать уведомления;
  • логировать события.

Совместное использование с React Router

RTK Query хорошо интегрируется с маршрутизацией.


Prefetch при навигации

import { api } from './services/api'

const prefetchPost = api.util.prefetch('getPost', 15, {
    force: true
})
<Link
    to="/posts/15"
    onMouseEn ter={() => dispatch(prefetchPost)}
>
    Открыть пост
</Link>

Данные начинают загружаться ещё до перехода между страницами.


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

import { useParams } from 'react-router-dom'

function PostPage() {
    const { id } = useParams()

    const { data, isLoading } = useGetPostQuery(id)

    if (isLoading) {
        return <div>Loading...</div>
    }

    return <div>{data.title}</div>
}

Инвалидация после навигации

После мутаций часто требуется обновление данных при переходах.

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

    invalidatesTags: ['Posts']
})

После возврата на список постов RTK Query автоматически инициирует повторную загрузку.


Интеграция с Form-библиотеками

RTK Query часто используется вместе с:

  • React Hook Form;
  • Formik;
  • Final Form.

Совместное использование с React Hook Form

Получение initial values

function ProfileForm() {
    const { data } = useGetProfileQuery()

    const {
        register,
        handleSubmit,
        reset
    } = useForm()

    useEffect(() => {
        if (data) {
            reset(data)
        }
    }, [data])

    const [updateProfile] = useUpdateProfileMutation()

    const onSub mit = async (values) => {
        await updateProfile(values)
    }

    return (
        <form onSub mit={handleSubmit(onSubmit)}>
            <input {...register('name')} />
            <input {...register('email')} />
            <button>Save</button>
        </form>
    )
}

Обработка серверных ошибок

const onSub mit = async (values) => {
    try {
        await updateProfile(values).unwrap()
    } catch (error) {
        setError('email', {
            type: 'server',
            message: error.data.message
        })
    }
}

RTK Query удобно комбинируется с validation layer формы.


Совместное использование с Axios

RTK Query по умолчанию использует fetchBaseQuery, однако возможно подключение Axios.


Создание axiosBaseQuery

import axios from 'axios'

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

            return { data: result.data }
        } catch (axiosError) {
            const err = axiosError

            return {
                error: {
                    status: err.response?.status,
                    data: err.response?.data
                }
            }
        }
    }

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

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

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

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

Когда Axios действительно нужен

Чаще всего Axios подключается для:

  • interceptors;
  • upload/download progress;
  • сложной обработки ошибок;
  • кастомных adapters;
  • автоматических retry;
  • legacy-инфраструктуры.

Во многих случаях fetchBaseQuery оказывается проще и легче.


Совместное использование с GraphQL

RTK Query способен работать не только с REST API.


GraphQL baseQuery

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
                }
            }
        }
    }

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

export const api = createApi({
    baseQuery: graphqlBaseQuery({
        baseUrl: '/graphql'
    }),

    endpoints: (builder) => ({
        getPosts: builder.query({
            query: () => ({
                body: `
                    query {
                        posts {
                            id
                            title
                        }
                    }
                `
            })
        })
    })
})

Особенности GraphQL + RTK Query

При использовании GraphQL:

  • теги кэша приходится проектировать вручную;
  • отсутствует нормализация сущностей;
  • часто используется transformResponse;
  • сложнее автоматизировать invalidation.

Тем не менее RTK Query остаётся удобным orchestration layer.


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

RTK Query не заменяет WebSocket, но отлично работает вместе с ним.


Обновление кэша через WebSocket

getMessages: builder.query({
    query: () => '/messages',

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

        try {
            await cacheDataLoaded

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

                updateCachedData((draft) => {
                    draft.push(message)
                })
            }
        } catch {}

        await cacheEntryRemoved

        socket.close()
    }
})

RTK Query как слой синхронизации

Популярная архитектура:

Технология Роль
RTK Query Первичная загрузка
WebSocket Real-time обновления
RTK Query cache Хранилище
React UI Отображение

Совместное использование с Socket.IO

import { io } from 'socket.io-client'

const socket = io('http://localhost:3000')
socket.on('newPost', (post) => {
    store.dispatch(
        api.util.updateQueryData(
            'getPosts',
            undefined,
            (draft) => {
                draft.unshift(post)
            }
        )
    )
})

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


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

RTK Query поддерживает server-side rendering.

Особенно часто используется совместно с:

  • Next.js;
  • Remix;
  • custom SSR solutions.

Совместное использование с Next.js

Предварительная загрузка данных

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

Ожидание завершения запросов

await Promise.all(
    store.dispatch(
        api.util.getRunningQueriesThunk()
    )
)

Гидратация состояния

import { HYDRATE } from 'next-redux-wrapper'

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

Типичная SSR-схема

  1. Сервер создаёт store.
  2. RTK Query выполняет запросы.
  3. Кэш сериализуется.
  4. Клиент получает hydrated state.
  5. React использует готовые данные без повторной загрузки.

Совместное использование с redux-persist

RTK Query может работать с persisted state, однако требует осторожности.


Persist только обычного Redux state

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

const persistConfig = {
    key: 'root',
    storage,
    whitelist: ['auth']
}

Кэш RTK Query обычно не сохраняется.


Почему persist API cache опасен

Проблемы:

  • устаревшие данные;
  • broken subscriptions;
  • invalid timestamps;
  • конфликт re-fetch логики;
  • рассинхронизация с сервером.

Когда persist cache допустим

Иногда persist используется для:

  • offline-first приложений;
  • mobile приложений;
  • read-heavy dashboards;
  • медленных API.

В таких случаях необходима ручная стратегия инвалидирования.


Совместное использование с Normalizr и Entity Adapter

RTK Query хранит данные в query cache, но иногда требуется нормализованная структура.


transformResponse + createEntityAdapter

import {
    createEntityAdapter
} from '@reduxjs/toolkit'

const usersAdapter = createEntityAdapter()

const initialState =
    usersAdapter.getInitialState()

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

    transformResponse(response) {
        return usersAdapter.setAll(
            initialState,
            response
        )
    }
})

Преимущества нормализации

Нормализация позволяет:

  • быстро искать сущности;
  • уменьшать дублирование;
  • ускорять выборки;
  • упрощать memoization;
  • строить relational state.

Когда нормализация не нужна

RTK Query уже содержит эффективный query cache.

Избыточная нормализация часто усложняет код без реальной пользы.


Интеграция с Zod и Yup

RTK Query хорошо сочетается с runtime validation.


Валидация ответа сервера

import { z } from 'zod'

const UserSchema = z.object({
    id: z.number(),
    name: z.string(),
    email: z.string()
})
getUser: builder.query({
    query: (id) => `/users/${id}`,

    transformResponse(response) {
        return UserSchema.parse(response)
    }
})

Защита от некорректного API

Валидация позволяет:

  • обнаруживать сломанные контракты;
  • контролировать backend errors;
  • предотвращать повреждение state;
  • упрощать debugging.

Совместное использование с MSW

Mock Service Worker стал практически стандартом для тестирования RTK Query.


Перехват API запросов

import { rest } from 'msw'

export const handlers = [
    rest.get('/api/posts', (req, res, ctx) => {
        return res(
            ctx.status(200),
            ctx.json([
                {
                    id: 1,
                    title: 'Post'
                }
            ])
        )
    })
]

Тестирование компонентов

render(<PostsPage />)

expect(
    await screen.findByText('Post')
).toBeInTheDocument()

Почему MSW особенно удобен с RTK Query

MSW:

  • не требует мокать fetch;
  • работает на уровне network;
  • поддерживает browser и Node.js;
  • подходит для integration testing;
  • не зависит от реализации baseQuery.

Совместное использование с React Suspense

RTK Query постепенно развивается в сторону поддержки Suspense.


Suspense-подход

<Suspense fallback={<Loader />}>
    <PostsPage />
</Suspense>

Особенности Suspense

При использовании Suspense:

  • loading state делегируется React;
  • уменьшается количество condition rendering;
  • UI становится декларативнее;
  • возрастает роль Error Boundary.

Интеграция с Error Boundaries

RTK Query ошибки удобно комбинируются с React Error Boundary.


Пример Error Boundary

class ErrorBoundary extends React.Component {
    state = {
        hasError: false
    }

    static getDerivedStateFromError() {
        return {
            hasError: true
        }
    }

    render() {
        if (this.state.hasError) {
            return <div>Ошибка</div>
        }

        return this.props.children
    }
}

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

if (error) {
    throw error
}

Ошибка передаётся в Error Boundary автоматически.


Совместное использование с Sentry

RTK Query удобно интегрируется с системами мониторинга.


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

async onQueryStarted(arg, api) {
    try {
        await api.queryFulfilled
    } catch (error) {
        Sentry.captureException(error)
    }
}

Интеграция с feature-sliced design

RTK Query хорошо вписывается в FSD-архитектуру.


Типичная структура

src/
├─ app/
├─ pages/
├─ widgets/
├─ features/
├─ entities/
│  ├─ user/
│  │  ├─ api/
│  │  ├─ model/
│  │  ├─ ui/

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

export const userApi = createApi({
    reducerPath: 'userApi',
    endpoints: () => ({})
})

Inject Endpoints

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

Такой подход особенно удобен для code splitting.


Совместное использование с monorepo

RTK Query хорошо подходит для monorepo-архитектур.


Shared API package

packages/
├─ api/
├─ web/
├─ admin/
├─ mobile/

Переиспользование endpoint definitions

export const api = createApi({
    baseQuery,
    endpoints: () => ({})
})
export const usersApi =
    api.injectEndpoints({
        endpoints: (builder) => ({
            getUsers: builder.query({
                query: () => '/users'
            })
        })
    })

Интеграция с microfrontend архитектурой

RTK Query может использоваться внутри независимых frontend-модулей.


Изоляция reducerPath

createApi({
    reducerPath: 'billingApi'
})
createApi({
    reducerPath: 'profileApi'
})

Каждый microfrontend получает собственный namespace.


Избежание конфликтов middleware

При большом количестве API slice важно:

  • избегать дублирующих reducerPath;
  • контролировать middleware order;
  • следить за количеством listeners;
  • ограничивать повторные polling subscriptions.

Совместное использование с Electron

RTK Query способен работать в desktop-приложениях.


Типичные сценарии

В Electron RTK Query используется для:

  • работы с remote API;
  • синхронизации данных;
  • offline cache;
  • обновления desktop dashboard;
  • background sync.

Интеграция с React Native

RTK Query поддерживает React Native практически без изменений.


Особенности mobile среды

На мобильных устройствах важно учитывать:

  • нестабильную сеть;
  • reconnect behavior;
  • aggressive cache reuse;
  • offline-first архитектуру;
  • ограничение трафика.

refetchOnReconnect

setupListeners(store.dispatch)
useGetPostsQuery(undefined, {
    refetchOnReconnect: true
})

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


Комбинирование RTK Query с другими state manager решениями

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


RTK Query + Zustand

const useUiStore = create((set) => ({
    modalOpen: false,

    setModalOpen: (value) =>
        set({
            modalOpen: value
        })
}))
const { data } = useGetUserQuery()

Zustand управляет UI state, RTK Query — серверными данными.


RTK Query + Jotai

const filterAtom = atom('')
const filter = useAtomValue(filterAtom)

const { data } = useGetPostsQuery(filter)

RTK Query + Recoil

const authState = atom({
    key: 'auth',
    default: null
})

RTK Query остаётся отдельным серверным слоем.


Стратегии гибридной архитектуры

На практике часто используются смешанные схемы.


Распространённая комбинация

Слой Инструмент
Серверный state RTK Query
UI state Zustand
Формы React Hook Form
Навигация React Router
Валидация Zod
WebSocket Socket.IO
Тестирование MSW

Причины популярности RTK Query в гибридной архитектуре

RTK Query:

  • не навязывает структуру приложения;
  • не требует полного Redux-first подхода;
  • легко внедряется постепенно;
  • совместим с legacy кодом;
  • поддерживает code splitting;
  • не конфликтует с другими state manager решениями;
  • позволяет централизовать сетевой слой.

Именно эта гибкость сделала RTK Query универсальным инструментом для современных frontend-приложений различного масштаба и архитектурной сложности.