Миграция с Redux Saga

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

Redux Saga строится вокруг генераторов и эффекта-ориентированной модели. Вся логика взаимодействия с API, кешированием, отменой запросов и обработкой гонок реализуется вручную через sagas:

  • takeEvery, takeLatest для контроля конкурентности
  • call для выполнения API-запросов
  • put для диспатча действий
  • ручное управление loading/error/success состояниями
  • явная нормализация и хранение данных

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

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

Ключевая трансформация заключается в переходе от «управления потоками событий» к «описанию источников данных».

Подготовка архитектуры перед миграцией

Перед переносом логики важно определить, какие части Saga-кода относятся к серверному состоянию:

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

Логика бизнес-процессов, не связанная напрямую с API (например, сложные последовательности действий пользователя, оркестрация нескольких независимых процессов), может остаться в saga или быть перенесена в middleware/RTK listener middleware.

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

Сравнение структуры кода

Saga-подход

Типичный flow загрузки данных в Redux Saga:

function* fetchUsersSaga() {
  try {
    yield put({ type: 'users/fetchStart' });

    const data = yield call(api.getUsers);

    yield put({ type: 'users/fetchSuccess', payload: data });
  } catch (e) {
    yield put({ type: 'users/fetchError', payload: e.message });
  }
}

Дополнительно требуется reducer:

const initialState = {
  data: [],
  loading: false,
  error: null
};

И подключение saga middleware.

RTK Query-подход

Тот же функционал:

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

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

export const { useGetUsersQuery } = api;

Все состояния loading, error, data создаются автоматически.

Маппинг концепций Redux Saga на RTK Query

1. Запрос данных

Saga:

yield call(api.getUsers)

RTK Query:

builder.query({
  query: () => '/users'
})

Запрос становится декларацией, а не инструкцией выполнения.


2. Управление loading/error state

Saga требует ручного обновления состояния:

put({ type: 'fetchStart' })
put({ type: 'fetchError' })

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

  • isLoading
  • isFetching
  • isSuccess
  • isError
  • error
const { data, isLoading, error } = useGetUsersQuery();

3. Кэширование

Saga обычно использует самописный кеш:

  • нормализация через normalizr
  • хранение по id
  • ручное обновление при мутациях

RTK Query использует встроенный кеш:

  • ключ = endpoint + аргументы
  • автоматическая дедупликация
  • TTL и garbage collection
  • инвалидация через tags

4. Инвалидация данных

Saga:

yield put({ type: 'USERS_INVALIDATE' })
yield call(fetchUsersSaga)

RTK Query:

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

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

Инвалидация становится декларативной.

Пошаговая стратегия миграции

1. Выделение API-слоя из Saga

Первый этап — извлечение всех API-вызовов из sagas:

function* fetchUsersSaga() {
  const data = yield call(api.getUsers);
}

Выделяется отдельный API-модуль:

const api = {
  getUsers: () => fetch('/users').then(r => r.json())
};

Затем этот слой переносится в RTK Query baseQuery.


2. Создание RTK Query API slice

Создается единый API slice:

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Users', 'Posts'],
  endpoints: () => ({})
});

3. Перенос query endpoints

Saga:

function* fetchUsersSaga() {
  const data = yield call(api.getUsers);
  yield put({ type: 'success', payload: data });
}

RTK Query:

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

4. Перенос mutation логики

Saga:

function* createUserSaga(action) {
  yield call(api.createUser, action.payload);
  yield put({ type: 'USERS_REFRESH' });
}

RTK Query:

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

5. Удаление ручного состояния

После миграции убираются:

  • reducers loading/error
  • action creators FETCH_START / FETCH_SUCCESS / FETCH_ERROR
  • селекторы, связанные с API state
  • saga watchers для API

Состояние становится производным от RTK Query cache.

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

Redux Saga часто использовалась не только для API, но и для сложных сценариев:

  • последовательные запросы
  • debounce/throttle
  • отмена запросов
  • orchestration workflows

RTK Query покрывает часть этих задач:

Дебаунс и троттлинг

useGetUsersQuery(searchTerm, {
  skip: searchTerm.length < 3
});

Либо через queryFn и кастомную логику.


Отмена запросов

RTK Query автоматически отменяет запрос при:

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

Saga требует ручного AbortController или cancellation effects.


Последовательные запросы

RTK Query позволяет комбинировать endpoints:

const user = useGetUserQuery(id);
const posts = useGetPostsQuery(user.data?.id, {
  skip: !user.data
});

Сложные сценарии могут быть вынесены в queryFn.

Интеграция RTK Query в существующий Redux Saga проект

Полная замена Saga не всегда требуется. Часто используется гибридная модель:

  • RTK Query для CRUD и server state
  • Saga для orchestration и сложных side-effects

Пример coexistence:

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

Saga продолжает обрабатывать:

  • WebSocket события
  • сложные бизнес-процессы
  • фоновые задачи

RTK Query берет:

  • API caching
  • data fetching
  • synchronization

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

1. Перенос всей логики Saga в RTK Query

RTK Query не предназначен для сложных workflow-цепочек. Попытка перенести orchestration приводит к перегруженным queryFn.


2. Дублирование состояния

Сохранять одновременно:

  • RTK Query cache
  • Redux slice data

создает рассинхронизацию.


3. Игнорирование invalidation

Отсутствие providesTags и invalidatesTags приводит к устаревшим данным.


4. Неправильное использование skip

Частая ошибка — попытка вручную контролировать загрузку там, где RTK Query уже управляет жизненным циклом запроса.

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

Saga требует мышления в терминах потоков событий и управления ими. RTK Query требует мышления в терминах:

  • данных (data)
  • источников (endpoints)
  • зависимостей (tags)
  • автоматической синхронизации

Переход фактически означает отказ от imperative control flow в пользу declarative data fetching layer.

Производственные эффекты миграции

После перехода обычно уменьшается:

  • количество Redux actions
  • объем reducer boilerplate
  • количество saga watchers
  • сложность управления loading/error states

И увеличивается:

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