RTK Query представляет собой высокоуровневую абстракцию для работы с серверным состоянием в Redux-приложениях, и при переходе с Redux Saga основное изменение заключается в смене парадигмы: от ручного управления побочными эффектами к декларативному описанию источников данных и автоматическому управлению кэшем, статусами запросов и синхронизацией состояния.
Redux Saga строится вокруг генераторов и эффекта-ориентированной модели. Вся логика взаимодействия с API, кешированием, отменой запросов и обработкой гонок реализуется вручную через sagas:
takeEvery, takeLatest для контроля
конкурентностиcall для выполнения API-запросовput для диспатча действийRTK Query устраняет необходимость в явном описании большинства этих механизмов. Вместо этого используется декларативное описание endpoints, а библиотека берет на себя:
Ключевая трансформация заключается в переходе от «управления потоками событий» к «описанию источников данных».
Перед переносом логики важно определить, какие части Saga-кода относятся к серверному состоянию:
Логика бизнес-процессов, не связанная напрямую с API (например, сложные последовательности действий пользователя, оркестрация нескольких независимых процессов), может остаться в saga или быть перенесена в middleware/RTK listener middleware.
RTK Query не заменяет полностью 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.
Тот же функционал:
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 создаются автоматически.
Saga:
yield call(api.getUsers)
RTK Query:
builder.query({
query: () => '/users'
})
Запрос становится декларацией, а не инструкцией выполнения.
Saga требует ручного обновления состояния:
put({ type: 'fetchStart' })
put({ type: 'fetchError' })
RTK Query автоматически предоставляет:
isLoadingisFetchingisSuccessisErrorerrorconst { data, isLoading, error } = useGetUsersQuery();
Saga обычно использует самописный кеш:
normalizrRTK Query использует встроенный кеш:
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']
})
Инвалидация становится декларативной.
Первый этап — извлечение всех API-вызовов из sagas:
function* fetchUsersSaga() {
const data = yield call(api.getUsers);
}
Выделяется отдельный API-модуль:
const api = {
getUsers: () => fetch('/users').then(r => r.json())
};
Затем этот слой переносится в RTK Query baseQuery.
Создается единый API slice:
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Users', 'Posts'],
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']
})
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']
})
После миграции убираются:
Состояние становится производным от RTK Query cache.
Redux Saga часто использовалась не только для API, но и для сложных сценариев:
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.
Полная замена Saga не всегда требуется. Часто используется гибридная модель:
Пример coexistence:
const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware, sagaMiddleware)
});
Saga продолжает обрабатывать:
RTK Query берет:
RTK Query не предназначен для сложных workflow-цепочек. Попытка
перенести orchestration приводит к перегруженным
queryFn.
Сохранять одновременно:
создает рассинхронизацию.
Отсутствие providesTags и invalidatesTags
приводит к устаревшим данным.
Частая ошибка — попытка вручную контролировать загрузку там, где RTK Query уже управляет жизненным циклом запроса.
Saga требует мышления в терминах потоков событий и управления ими. RTK Query требует мышления в терминах:
Переход фактически означает отказ от imperative control flow в пользу declarative data fetching layer.
После перехода обычно уменьшается:
И увеличивается: