Next.js интеграция

RTK Query в экосистеме Next.js используется для построения предсказуемого слоя получения и кэширования данных в приложениях с серверным рендерингом, статической генерацией и гибридными режимами. Основная сложность интеграции заключается в том, что RTK Query изначально рассчитан на клиентский Redux Store, тогда как Next.js оперирует разными контекстами выполнения: сервер, клиент, ISR/SSG-фазы, а также потоковый рендеринг в современных версиях.

RTK Query опирается на единый Redux Store, в котором хранится кэш запросов, метаданные и состояния загрузки. В Next.js возникает необходимость учитывать:

  • изоляцию состояния между запросами на сервере
  • повторное использование кэша на клиенте
  • гидратацию состояния после SSR
  • отсутствие глобального состояния между запросами

Ключевая идея интеграции заключается в создании фабрики Redux Store для каждого запроса на сервере и корректной передаче состояния в клиентский слой через hydration.

Конфигурация Redux Store для Next.js

Базовая настройка начинается с создания 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 (Next.js 13+)

В App Router используется React Server Components, что требует разделения server и client частей Redux.

Provider на клиентской стороне

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 при каждом рендере.

Подключение в layout

import ReduxProvider from './ReduxProvider'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ReduxProvider>
          {children}
        </ReduxProvider>
      </body>
    </html>
  )
}

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

RTK Query поддерживает prefetch через dispatch запросов до рендера страницы. В Next.js Pages Router используется getServerSideProps или getStaticProps.

SSR через getServerSideProps

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.

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

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/hydrate
  • merge кешей без дублирования запросов

Использование createApi в Next.js

API слой остаётся стандартным, но важно учитывать поведение кэша при 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 это означает:

  • одинаковые запросы на сервере дедуплицируются
  • кэш не сохраняется между запросами пользователей
  • на клиенте кэш восстанавливается из hydrated state

Prefetch данных на уровне компонентов

В 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 автоматически:

  • запускает запрос при монтировании
  • кеширует результат
  • повторно использует данные при повторном переходе

Оптимизация SSR: избегание дублирующих запросов

В Next.js при неправильной конфигурации возможно:

  • двойное выполнение запроса (server + client)
  • несинхронизированный cache hydration

Для предотвращения используется skipToken и условные запросы.

const { data } = useGetPostsQuery(undefined, {
  skip: typeof window === 'undefined',
})

Однако в SSR-подходе правильнее использовать prefetch и hydration, а не skip на клиенте.

Интеграция с Server Actions и App Router

В App Router можно комбинировать RTK Query и server actions, но важно разделять ответственность:

  • server actions: мутации на сервере
  • RTK Query: клиентский кэш и синхронизация

Пример 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'],
})

Теги и инвалидация в Next.js

RTK Query использует систему тегов для управления кэшем:

getPosts: builder.query({
  query: () => '/posts',
  providesTags: ['Posts'],
})
addPost: builder.mutation({
  query: (post) => ({
    url: '/posts',
    method: 'POST',
    body: post,
  }),
  invalidatesTags: ['Posts'],
})

В Next.js это особенно важно, так как:

  • серверный кэш не живёт между запросами
  • клиентский кэш должен быть синхронизирован после навигации
  • инвалидация предотвращает устаревшие данные после переходов

Разделение клиентского и серверного API слоя

В современных Next.js архитектурах часто разделяют API:

  • server-only fetch для SSR
  • RTK Query для клиентской интерактивности

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

  • уменьшить нагрузку на Redux на сервере
  • избежать лишних сериализаций
  • ускорить гидратацию

Работа с cookies и авторизацией

В SSR важно передавать cookies в baseQuery:

baseQuery: fetchBaseQuery({
  baseUrl: '/api',
  credentials: 'include',
  prepareHeaders: (headers, { getState }) => {
    return headers
  },
})

При server-side запросах необходимо вручную прокидывать cookie из контекста запроса Next.js, иначе авторизация будет потеряна.

Потоковый рендеринг и RTK Query

В React 18 streaming SSR RTK Query ведёт себя следующим образом:

  • запросы могут начинаться до полной гидратации
  • кэш может частично заполняться по мере стриминга
  • hydration происходит инкрементально

Это требует стабильного store на каждый request lifecycle и отсутствия глобального состояния.

Типовые ошибки интеграции

  • использование singleton store на сервере
  • отсутствие api.middleware в store
  • неправильная гидратация preloadedState
  • дублирование запросов при mount + SSR
  • несинхронизированная инвалидация тегов

Каждая из этих ошибок приводит либо к утечкам состояния между пользователями, либо к лишним сетевым запросам, либо к рассинхронизации UI.

Роль RTK Query в архитектуре Next.js приложения

RTK Query в связке с Next.js выполняет роль промежуточного слоя между серверным рендерингом и клиентской интерактивностью, обеспечивая:

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

При корректной архитектуре он становится центральным элементом data-layer приложения, вокруг которого строится SSR, SSG и клиентская логика обновлений данных