Миграция с Redux Thunk

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

Redux Thunk реализует асинхронную логику вручную через функции-диспетчеры. Типичный поток включает:

  • dispatch начала запроса
  • выполнение fetch/axios
  • dispatch успеха или ошибки
  • ручное обновление store

RTK Query заменяет этот подход декларативной моделью:

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

Ключевое различие заключается в том, что Redux Thunk управляет процессом, а RTK Query описывает данные и их получение.


Анализ существующей архитектуры перед миграцией

Перед переходом важно разделить существующие thunk-логики на категории:

Запросы данных (server state)

  • загрузка списков
  • получение сущностей
  • фильтрация через API
  • пагинация

Именно эта категория является основной целью миграции в RTK Query.

Локальная бизнес-логика

  • сложные вычисления
  • трансформации данных
  • цепочки действий внутри приложения

Такая логика обычно остаётся в reducers или middleware.

Смешанные случаи

  • thunk, который загружает данные и сразу нормализует их
  • thunk, который вызывает несколько API подряд

Именно смешанные случаи требуют рефакторинга перед переходом.


Подготовка RTK Query слоя

Основной шаг миграции — создание API-сервиса через createApi.

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

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

На этом этапе важно определить базовый URL и единый механизм запросов. В большинстве случаев fetchBaseQuery заменяет axios-инстансы, используемые в thunk-слое.


Перенос простого thunk-запроса

Исходный thunk:

export const fetchUsers = () => async (dispatch) => {
  dispatch(usersLoading());

  try {
    const response = await fetch('/api/users');
    const data = await response.json();

    dispatch(usersSuccess(data));
  } catch (e) {
    dispatch(usersError(e.toString()));
  }
};

RTK Query эквивалент:

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

Ключевое изменение:

  • исчезает ручной dispatch
  • исчезает обработка loading/error
  • исчезает хранение данных в usersSlice

Интеграция API slice в store

При миграции важно правильно подключить reducer и middleware:

import { configureStore } from '@reduxjs/toolkit';
import { api } from './api';

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

Middleware RTK Query обеспечивает:

  • кеширование
  • дедупликацию запросов
  • автообновление данных
  • фоновые рефетчи

Замена selectors и useEffect-загрузок

В thunk-подходе данные часто загружались через useEffect:

useEffect(() => {
  dispatch(fetchUsers());
}, []);

В RTK Query это заменяется хуком:

const { data, error, isLoading } = useGetUsersQuery();

Изменяется сам принцип:

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

Работа с параметризованными запросами

Thunk:

export const fetchUserById = (id) => async (dispatch) => {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
};

RTK Query:

getUserById: builder.query({
  query: (id) => `/users/${id}`,
});

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

const { data } = useGetUserByIdQuery(userId);

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


Инвалидация данных вместо ручного refetch

В thunk-архитектуре обновление данных требует ручного повторного запроса:

  • dispatch(fetchUsers()) после мутации
  • либо сложные цепочки действий

RTK Query использует систему тегов:

getUsers: builder.query({
  query: () => '/users',
  providesTags: ['Users'],
});

addUser: builder.mutation({
  query: (body) => ({
    url: '/users',
    method: 'POST',
    body,
  }),
  invalidatesTags: ['Users'],
});

После добавления пользователя список обновляется автоматически.


Замена thunk-мутаций

Исходный thunk:

export const createUser = (user) => async (dispatch) => {
  const res = await fetch('/api/users', {
    method: 'POST',
    body: JSON.stringify(user),
  });

  const data = await res.json();
  dispatch(userCreated(data));
};

RTK Query:

addUser: builder.mutation({
  query: (user) => ({
    url: '/users',
    method: 'POST',
    body: user,
  }),
});

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

const [addUser] = useAddUserMutation();

Удаление старых slices

После переноса endpoints становится возможным удалить:

  • loading state в slice
  • error state
  • reducers, связанные с серверными данными
  • thunk-файлы

Остаются только:

  • UI state
  • локальные вычисления
  • бизнес-логика, не связанная с API

Переход от normalize state к cache-based модели

Redux Thunk часто использует:

  • entities
  • normalized state
  • ручные редьюсеры

RTK Query заменяет это кэшем:

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

Это меняет подход к проектированию store: серверное состояние больше не моделируется вручную.


Обработка побочных эффектов после миграции

В thunk архитектуре побочные эффекты часто выполнялись внутри async функций.

В RTK Query они реализуются через:

  • onQueryStarted
  • onCacheEntryAdded

Пример:

getUsers: builder.query({
  query: () => '/users',
  async onQueryStarted(arg, { queryFulfilled }) {
    try {
      await queryFulfilled;
    } catch (e) {
      console.log('error');
    }
  },
});

Частичная миграция и гибридный режим

Миграция не требует полного отказа от thunk сразу. Возможна гибридная архитектура:

  • RTK Query для server state
  • Thunk для сложной бизнес-логики

Например:

  • RTK Query: CRUD пользователей
  • Thunk: многошаговые сценарии регистрации, аналитика, цепочки действий

Типичные ошибки при миграции

Попытка сохранить старый slice параллельно с RTK Query

Это приводит к дублированию данных и рассинхронизации состояния.

Использование RTK Query как замены бизнес-логики

RTK Query не предназначен для сложных вычислений или orchestration логики.

Игнорирование тегов

Без providesTags и invalidatesTags теряется основное преимущество автоматической актуализации данных.

Перенос thunk 1:1 без рефакторинга

Правильная миграция требует пересмотра архитектуры, а не механической замены.


Роль middleware и DevTools после миграции

RTK Query расширяет Redux DevTools:

  • отображение запросов
  • состояние кэша
  • lifecycle запросов
  • повторные попытки

Middleware больше не управляет асинхронностью вручную, а становится инфраструктурным слоем RTK Query.


Изменение мышления при переходе

Переход с Redux Thunk на RTK Query фактически меняет модель работы с данными:

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

Архитектура становится ориентированной на данные, а не на действия.