RTK Query в экосистеме Next.js используется для построения предсказуемого слоя получения и кэширования данных в приложениях с серверным рендерингом, статической генерацией и гибридными режимами. Основная сложность интеграции заключается в том, что RTK Query изначально рассчитан на клиентский Redux Store, тогда как Next.js оперирует разными контекстами выполнения: сервер, клиент, ISR/SSG-фазы, а также потоковый рендеринг в современных версиях.
RTK Query опирается на единый Redux Store, в котором хранится кэш запросов, метаданные и состояния загрузки. В Next.js возникает необходимость учитывать:
Ключевая идея интеграции заключается в создании фабрики Redux Store для каждого запроса на сервере и корректной передаче состояния в клиентский слой через hydration.
Базовая настройка начинается с создания API слоя RTK Query и подключения его reducer и middleware.
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { api } from './services/api'
export const makeStore = () => {
const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
})
setupListeners(store.dispatch)
return store
}
Важно, что store создаётся через функцию, а не как singleton. Это критично для SSR, где каждый запрос должен иметь изолированный экземпляр состояния.
В App Router используется React Server Components, что требует разделения server и client частей Redux.
Redux Provider должен быть клиентским компонентом:
'use client'
import { Provider } from 'react-redux'
import { makeStore } from './store'
import { useRef } from 'react'
export default function ReduxProvider({ children }) {
const storeRef = useRef()
if (!storeRef.current) {
storeRef.current = makeStore()
}
return (
<Provider store={storeRef.current}>
{children}
</Provider>
)
}
Использование useRef предотвращает пересоздание store при каждом рендере.
import ReduxProvider from './ReduxProvider'
export default function RootLayout({ children }) {
return (
<html>
<body>
<ReduxProvider>
{children}
</ReduxProvider>
</body>
</html>
)
}
RTK Query поддерживает prefetch через dispatch запросов до рендера страницы. В Next.js Pages Router используется getServerSideProps или getStaticProps.
import { makeStore } from '../store'
import { api } from '../services/api'
export async function getServerSideProps() {
const store = makeStore()
await store.dispatch(
api.endpoints.getPosts.initiate()
)
return {
props: {
initialState: store.getState(),
},
}
}
На клиенте состояние восстанавливается через preloadedState.
Hydration обеспечивает перенос кэша RTK Query с сервера на клиент.
import { configureStore } from '@reduxjs/toolkit'
import { api } from './services/api'
export function makeStore(preloadedState) {
return configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
preloadedState,
})
}
На клиенте передаётся состояние из SSR:
const store = makeStore(window.__PRELOADED_STATE__)
RTK Query автоматически использует hydration механизм через внутренние actions:
api/util/hydrateAPI слой остаётся стандартным, но важно учитывать поведение кэша при SSR.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: 'https://example.com/api',
}),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
}),
}),
})
RTK Query кэширует данные по ключу запроса. В SSR это означает:
В Next.js часто используется подход предварительной загрузки данных при навигации.
'use client'
import { useGetPostsQuery } from '../services/api'
export default function PostsPage() {
const { data, isLoading } = useGetPostsQuery()
if (isLoading) return 'loading'
return (
<div>
{data.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}
RTK Query автоматически:
В Next.js при неправильной конфигурации возможно:
Для предотвращения используется skipToken и условные запросы.
const { data } = useGetPostsQuery(undefined, {
skip: typeof window === 'undefined',
})
Однако в SSR-подходе правильнее использовать prefetch и hydration, а не skip на клиенте.
В App Router можно комбинировать RTK Query и server actions, но важно разделять ответственность:
Пример server action:
'use server'
export async function createPost(data) {
await fetch('https://example.com/api/posts', {
method: 'POST',
body: JSON.stringify(data),
})
}
После выполнения server action RTK Query может инвалидировать кэш:
builder.mutation({
query: (newPost) => ({
url: '/posts',
method: 'POST',
body: newPost,
}),
invalidatesTags: ['Posts'],
})
RTK Query использует систему тегов для управления кэшем:
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Posts'],
})
addPost: builder.mutation({
query: (post) => ({
url: '/posts',
method: 'POST',
body: post,
}),
invalidatesTags: ['Posts'],
})
В Next.js это особенно важно, так как:
В современных Next.js архитектурах часто разделяют API:
Такой подход позволяет:
В SSR важно передавать cookies в baseQuery:
baseQuery: fetchBaseQuery({
baseUrl: '/api',
credentials: 'include',
prepareHeaders: (headers, { getState }) => {
return headers
},
})
При server-side запросах необходимо вручную прокидывать cookie из контекста запроса Next.js, иначе авторизация будет потеряна.
В React 18 streaming SSR RTK Query ведёт себя следующим образом:
Это требует стабильного store на каждый request lifecycle и отсутствия глобального состояния.
Каждая из этих ошибок приводит либо к утечкам состояния между пользователями, либо к лишним сетевым запросам, либо к рассинхронизации UI.
RTK Query в связке с Next.js выполняет роль промежуточного слоя между серверным рендерингом и клиентской интерактивностью, обеспечивая:
При корректной архитектуре он становится центральным элементом data-layer приложения, вокруг которого строится SSR, SSG и клиентская логика обновлений данных