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

Гидратация состояния в RTK Query представляет собой механизм восстановления кэша запросов на клиенте после серверного рендеринга или после передачи предзагруженного состояния Redux. Основная задача этого процесса — синхронизировать данные, полученные на сервере, с клиентским хранилищем без повторных запросов и потери актуальности уже загруженных сущностей.

RTK Query хранит данные в нормализованном виде внутри Redux store. Каждый запрос идентифицируется комбинацией:

  • endpoint name
  • аргументов запроса
  • сериализованного ключа cache key

Кэш включает:

  • данные ответа
  • статус загрузки (pending, fulfilled, rejected)
  • метаданные подписок
  • timestamp последнего обновления

При SSR (Server-Side Rendering) формируется предварительно заполненный Redux store, который затем передаётся на клиент. Именно в этот момент возникает необходимость гидратации: клиент должен корректно объединить серверное состояние с собственным экземпляром store.

Проблема несинхронизированного кэша

Без гидратации возникают типичные проблемы:

  • повторные запросы при монтировании компонентов
  • потеря серверных данных при инициализации клиента
  • расхождение состояния между HTML, сгенерированным сервером, и клиентским DOM
  • лишние network roundtrips

RTK Query решает эти проблемы через механизм rehydration, который позволяет “влить” серверный кэш в клиентский store без нарушения целостности уже существующих данных.

Базовый механизм гидратации

В основе лежит Redux action, который передаёт сериализованное состояние store:

const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(api.middleware),
  preloadedState: window.__PRELOADED_STATE__,
});

На сервере формируется preloadedState, содержащий кэш RTK Query. На клиенте этот объект используется как начальное состояние Redux.

Однако этого недостаточно, так как RTK Query требует корректного объединения (merge) внутренних структур кэша.

HYDRATE action и next-redux-wrapper

В экосистеме Next.js часто используется next-redux-wrapper, который вводит специальный action:

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

Этот action применяется для объединения серверного и клиентского состояния:

const rootReducer = (state, action) => {
  switch (action.type) {
    case HYDRATE:
      return {
        ...state,
        ...action.payload,
      };
    default:
      return appReducer(state, action);
  }
};

Но при работе с RTK Query такой поверхностный merge недостаточен, поскольку кэш API имеет вложенную структуру с ключами запросов и метаданными.

extractRehydrationInfo как основной механизм RTK Query

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

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

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

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

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

Этот механизм выполняет ключевую функцию:

  • извлекает только часть состояния, относящуюся к конкретному API slice
  • предотвращает перезапись других частей store
  • обеспечивает корректное объединение кэша

Структура состояния RTK Query при гидратации

Состояние RTK Query обычно выглядит так:

{
  queries: {
    "getPosts(undefined)": {
      status: "fulfilled",
      endpointName: "getPosts",
      data: [...],
      fulfilledTimeStamp: 123456789
    }
  },
  mutations: {},
  provided: {},
  subscriptions: {},
  config: {}
}

При гидратации важно, чтобы:

  • queries не перетирались полностью
  • существующие подписки сохранялись
  • timestamps корректно обновлялись
  • дублирующие запросы не создавались повторно

Слияние кэша и стратегия merge

RTK Query использует стратегию глубокого объединения состояния API slice. При гидратации:

  1. серверный кэш добавляется в client store
  2. существующие query keys сравниваются
  3. при совпадении ключей выполняется проверка актуальности
  4. более свежие данные сохраняются

Особенно важно поведение при конфликте:

  • если клиент уже имеет query в состоянии fulfilled, серверный результат может быть проигнорирован
  • если клиент не имеет данных — серверный кэш становится источником истины
  • если timestamps различаются — применяется более новый

Гидратация и сериализация query keys

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

serializeQueryArgs: ({ endpointName, queryArgs }) => {
  return `${endpointName}/${JSON.stringify(queryArgs)}`;
};

При SSR и hydration важно, чтобы:

  • сериализация была детерминированной
  • порядок ключей в объектах не влиял на результат
  • одинаковые запросы имели одинаковый cache key на сервере и клиенте

Несовпадение сериализации приводит к эффекту “дублирования кэша”, когда данные существуют дважды в store.

Гидратация и lazy queries

Lazy queries добавляют дополнительную сложность. При SSR:

  • запрос может быть уже выполнен на сервере
  • клиент не должен повторно инициировать fetch
  • состояние lazy trigger должно быть восстановлено корректно

RTK Query хранит информацию о подписках, поэтому при гидратации:

  • восстанавливаются активные subscriptions
  • неактивные lazy triggers не инициируются автоматически
  • выполняется только восстановление кэша, а не повторный execution pipeline

SSR сценарий с RTK Query

Типичный поток:

  1. создание store на сервере
  2. выполнение dispatch(api.endpoints.getPosts.initiate())
  3. ожидание завершения через await
  4. извлечение состояния через store.getState()
  5. передача состояния в клиент
await store.dispatch(api.endpoints.getPosts.initiate());

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

После этого:

const preloadedState = store.getState();

На клиенте этот state становится источником гидратации.

Важность getRunningQueriesThunk

Этот thunk обеспечивает завершение всех активных запросов перед сериализацией состояния. Без него возможны:

  • неполные данные в кэше
  • незавершённые queries в состоянии pending
  • расхождение между сервером и клиентом
await Promise.all(store.dispatch(api.util.getRunningQueriesThunk()));

Поведение refetch после гидратации

После восстановления состояния RTK Query применяет стандартные политики:

  • refetchOnMountOrArgChange
  • refetchOnFocus
  • refetchOnReconnect

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

Если данные:

  • свежие → повторный запрос не выполняется
  • устаревшие → выполняется background refetch

Конфликты состояния и стратегии разрешения

Возможны ситуации, когда:

  • сервер вернул одни данные
  • клиент уже имеет более новые данные
  • запрос был выполнен на клиенте до гидратации

RTK Query решает это через timestamp-based reconciliation:

  • fulfilledTimeStamp определяет актуальность
  • более поздний timestamp имеет приоритет
  • данные с одинаковыми ключами не дублируются

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

Гидратация не отключает механизм tag invalidation. После восстановления:

  • теги providesTags остаются активными
  • invalidateTags может инициировать refetch
  • состояние cache lifecycle продолжает работать штатно

Это важно для динамических систем, где SSR даёт только стартовое состояние, а актуальность обеспечивается клиентом.

Гидратация в многомодульных API slices

При использовании нескольких createApi slices:

  • каждый slice гидратируется независимо
  • extractRehydrationInfo должен учитывать reducerPath
  • объединение происходит на уровне root store
if (action.type === HYDRATE) {
  return action.payload[reducerPath];
}

Ошибки в этом месте приводят к:

  • потере части кэша
  • конфликтам между API slices
  • неконсистентному состоянию приложения

Оптимизация гидратации и памяти

RTK Query хранит значительный объём метаданных. При гидратации важно учитывать:

  • избыточные queries увеличивают memory footprint
  • неиспользуемые endpoints могут быть очищены через api.util.resetApiState()
  • SSR кэш следует минимизировать до реально нужных данных

Также полезна стратегия selective prefetch:

  • гидратируются только критические данные страницы
  • второстепенные запросы выполняются на клиенте

Типичные ошибки при реализации гидратации

  • поверхностный merge Redux state без учёта RTK Query структуры
  • отсутствие extractRehydrationInfo
  • несинхронизированная сериализация query args
  • отсутствие ожидания getRunningQueriesThunk
  • дублирование API slice reducerPath
  • попытка гидратации без SSR-совместимого store lifecycle

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

Поведение при частичной гидратации

RTK Query поддерживает частичную гидратацию:

  • часть endpoints может отсутствовать в payload
  • отсутствующие данные считаются “не закэшированными”
  • такие queries инициируют fetch при монтировании

Это поведение используется в гибридных SSR/CSR приложениях, где сервер рендерит только ключевые данные страницы.