SSR (Server-Side Rendering) накладывает на RTK Query ряд особенностей, связанных с жизненным циклом запроса, изоляцией состояния Redux на каждый запрос пользователя и необходимостью корректной гидрации клиентского кэша. Основная цель интеграции — обеспечить одинаковые данные на сервере и клиенте без повторного запроса там, где это возможно, сохранив при этом преимущества кэширования RTK Query.
RTK Query опирается на Redux store как на источник единого состояния. В SSR-приложениях это означает, что:
Ключевой момент заключается в том, что RTK Query не хранит состояние вне Redux. Поэтому SSR-интеграция сводится к правильной организации store и синхронизации кэша.
В SSR нельзя использовать глобальный store. Причина — конкуренция запросов и утечки данных между пользователями.
Типовая фабрика store:
import { configureStore } from '@reduxjs/toolkit'
import { api } from './api'
export function createStore(preloadedState) {
return configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
preloadedState,
})
}
Каждый запрос на сервере вызывает createStore,
обеспечивая изоляцию состояния RTK Query.
Основной механизм SSR с RTK Query — ручной запуск запросов до рендера React.
RTK Query предоставляет .initiate() для dispatch
запросов без React-хуков.
import { api } from './api'
export async function loadData(store) {
const promise = store.dispatch(
api.endpoints.getPosts.initiate()
)
await promise
store.dispatch(api.util.getRunningQueriesThunk())
}
initiate() возвращает thunk, который можно ожидать через
.unwrap() или через Promise, доступный в dispatch.
await store.dispatch(api.endpoints.getPosts.initiate()).unwrap()
Это гарантирует, что к моменту рендера HTML данные уже находятся в кэше RTK Query.
В классическом Next.js SSR используется
getServerSideProps.
export async function getServerSideProps() {
const store = createStore()
await store.dispatch(api.endpoints.getPosts.initiate())
return {
props: {
initialReduxState: store.getState(),
},
}
}
На клиенте состояние передается в store как
preloadedState.
const store = createStore(initialReduxState)
RTK Query хранит данные в slice api.reducerPath. При
гидрации важно восстановить именно этот сегмент состояния.
import { Provider } from 'react-redux'
export function App({ store }) {
return (
<Provider store={store}>
<Root />
</Provider>
)
}
Redux Toolkit автоматически подхватывает preloadedState,
и RTK Query считает кэш уже заполненным.
Без дополнительных настроек RTK Query может повторно запрашивать данные после гидрации. Это связано с тем, что:
Решение — корректная настройка refetchOnMountOrArgChange
и refetchOnFocus.
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
immutableCheck: false,
serializableCheck: false,
}).concat(api.middleware),
})
И подключение listeners:
import { setupListeners } from '@reduxjs/toolkit/query'
setupListeners(store.dispatch)
RTK Query различает несколько уровней состояния:
При SSR важно помнить:
Поэтому часто используют принудительную очистку store после запроса, если он живёт дольше одного рендера.
В App Router подход отличается: серверные компоненты позволяют выполнять запросы напрямую, но RTK Query используется через клиентский слой или через prefetch в server actions.
Типовая схема:
const store = createStore()
await store.dispatch(api.endpoints.getUser.initiate(userId))
const state = store.getState()
Далее state передаётся в клиентский компонент.
RTK Query поддерживает кастомную логику восстановления через
extractRehydrationInfo, особенно в связке с Next.js и redux
wrappers.
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
}),
}),
extractRehydrationInfo(action, { reducerPath }) {
if (action.type === 'HYDRATE') {
return action.payload[reducerPath]
}
},
})
Это позволяет корректно восстановить кэш RTK Query при серверной гидрации.
SSR не отменяет необходимость работы с инвалидацией. Однако есть отличие:
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Posts'],
}),
})
При мутациях на клиенте RTK Query автоматически обновляет связанные запросы.
Для минимизации времени ответа используются:
await Promise.all([
store.dispatch(api.endpoints.getUser.initiate(userId)),
store.dispatch(api.endpoints.getSettings.initiate()),
])
RTK Query объединяет одинаковые запросы, поэтому дублирование не приводит к лишним HTTP вызовам.
При передаче state с сервера необходимо учитывать:
Особенно важно не нарушать serializableCheck, иначе hydration может сломаться.
В SSR-приложениях переходы между страницами используют уже гидратированный store. RTK Query:
Частая проблема — передача токена на сервер:
const baseQuery = fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
if (token) {
headers.set('authorization', `Bearer ${token}`)
}
return headers
},
})
В SSR токен часто берётся из cookies запроса и передаётся в createStore контекстом.
Каждый SSR-запрос должен:
Иначе возможны ситуации, когда данные одного пользователя попадают в HTML другого.
RTK Query поддерживает polling и подписки, но в SSR:
Поэтому сервер используется только для первичного заполнения кэша.
На практике чаще всего встречаются:
Каждая из этих ошибок приводит либо к утечкам данных, либо к удвоению запросов.
RTK Query в SSR строится вокруг принципа:
Модель позволяет избежать “водопадов” запросов на клиенте и ускоряет первичную отрисовку интерфейса за счёт уже готового состояния.