Многие проекты на React и Redux формировались задолго до появления RTK Query. Внутри таких приложений обычно уже существуют:
Полная одномоментная миграция практически всегда несёт риски:
Поэтому RTK Query обычно внедряется постепенно, по модулям и функциональным областям.
Наиболее безопасная стратегия выглядит следующим образом:
Такой подход позволяет:
npm install @reduxjs/toolkit react-redux
Допустим, проект уже использует Redux.
Старый store:
import { configureStore } from '@reduxjs/toolkit'
import authReducer from './authSlice'
import postsReducer from './postsSlice'
export const store = configureStore({
reducer: {
auth: authReducer,
posts: postsReducer,
},
})
Добавление RTK Query:
import { configureStore } from '@reduxjs/toolkit'
import { api } from './services/api'
import authReducer from './authSlice'
import postsReducer from './postsSlice'
export const store = configureStore({
reducer: {
auth: authReducer,
posts: postsReducer,
[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
})
Старые reducer продолжают работать без изменений.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: 'https://jsonplaceholder.typicode.com',
}),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
}),
}),
})
export const {
useGetPostsQuery,
} = api
Во время миграции обе технологии могут использоваться одновременно.
import axios from 'axios'
export const fetchPosts = () => async (dispatch) => {
dispatch({ type: 'posts/loading' })
try {
const response = await axios.get('/api/posts')
dispatch({
type: 'posts/success',
payload: response.data,
})
} catch (error) {
dispatch({
type: 'posts/error',
payload: error.message,
})
}
}
getPosts: builder.query({
query: () => '/posts',
})
Оба механизма могут существовать параллельно до полного удаления thunk.
Обычно миграция выполняется по экранам или функциональным модулям.
Пример безопасного порядка:
| Этап | Что переносится |
|---|---|
| 1 | Read-only страницы |
| 2 | Списки |
| 3 | Детальные страницы |
| 4 | Формы редактирования |
| 5 | Сложные mutation |
| 6 | Legacy middleware |
Query значительно проще mutation.
Причины:
Поэтому миграция обычно начинается с GET-запросов.
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { fetchPosts } from './postsThunk'
export function PostsPage() {
const dispatch = useDispatch()
const posts = useSelector((state) => state.posts.items)
const loading = useSelector((state) => state.posts.loading)
useEffect(() => {
dispatch(fetchPosts())
}, [dispatch])
if (loading) {
return <div>Loading...</div>
}
return (
<div>
{posts.map((post) => (
<div key={post.id}>
{post.title}
</div>
))}
</div>
)
}
import { useGetPostsQuery } from './services/api'
export function PostsPage() {
const {
data: posts,
isLoading,
} = useGetPostsQuery()
if (isLoading) {
return <div>Loading...</div>
}
return (
<div>
{posts.map((post) => (
<div key={post.id}>
{post.title}
</div>
))}
</div>
)
}
Количество кода сокращается в несколько раз.
Во многих проектах существует отдельный слой API:
import axios from 'axios'
export const postsApi = {
async getPosts() {
const response = await axios.get('/api/posts')
return response.data
},
async createPost(data) {
const response = await axios.post('/api/posts', data)
return response.data
},
}
RTK Query может использовать этот слой без переписывания backend-клиента.
import { createApi } from '@reduxjs/toolkit/query/react'
import { postsApi } from './postsApi'
export const api = createApi({
reducerPath: 'api',
baseQuery: async () => ({ data: null }),
endpoints: (builder) => ({
getPosts: builder.query({
async queryFn() {
try {
const data = await postsApi.getPosts()
return { data }
} catch (error) {
return {
error: {
status: 500,
data: error.message,
},
}
}
},
}),
}),
})
Это позволяет:
Redux Saga часто используется в старых enterprise-приложениях.
Типичный saga-запрос:
function* fetchPostsSaga() {
try {
const response = yield call(api.getPosts)
yield put({
type: 'posts/success',
payload: response.data,
})
} catch (error) {
yield put({
type: 'posts/error',
payload: error.message,
})
}
}
После миграции логика запроса исчезает:
getPosts: builder.query({
query: () => '/posts',
})
Saga остаётся только для сложной бизнес-логики:
RTK Query не обязан полностью заменять Saga.
Очень распространённая архитектура:
| Задача | Технология |
|---|---|
| HTTP cache | RTK Query |
| Fetch данных | RTK Query |
| CRUD | RTK Query |
| Websocket orchestration | Saga |
| Сложные процессы | Saga |
После переноса endpoint старые reducer становятся ненужными.
const initialState = {
items: [],
loading: false,
error: null,
}
RTK Query уже хранит:
Поэтому reducer можно удалить полностью.
До RTK Query:
POSTS_REQUEST
POSTS_SUCCESS
POSTS_ERROR
После RTK Query:
Старый код:
export const selectPosts = (state) => state.posts.items
Новый код:
const { data } = useGetPostsQuery()
Либо:
api.endpoints.getPosts.select()
Во время миграции часть состояния остаётся в Redux slices.
Пример:
{
auth,
settings,
ui,
api,
}
RTK Query отвечает только за серверное состояние.
UI-state продолжает жить отдельно.
RTK Query предназначен для server-state.
Не рекомендуется хранить там:
Mutation сложнее query.
Особенно если используются:
Только query.
Простые mutation:
createPost
deletePost
updatePost
Optimistic upd ate.
Сложные workflow.
export const createPost = (data) => async (dispatch) => {
dispatch({ type: 'create/loading' })
try {
const response = await axios.post('/posts', data)
dispatch({
type: 'create/success',
payload: response.data,
})
} catch (error) {
dispatch({
type: 'create/error',
payload: error.message,
})
}
}
createPost: builder.mutation({
query: (body) => ({
url: '/posts',
method: 'POST',
body,
}),
})
Старый подход:
dispatch(fetchPosts())
После создания записи.
RTK Query:
providesTags: ['Posts'],
invalidatesTags: ['Posts'],
Автоматическое обновление заменяет ручной refetch.
Старые приложения часто имеют:
RTK Query уже содержит:
Многие legacy-механизмы становятся ненужными.
Во время миграции может возникнуть ситуация:
Это опасно рассинхронизацией.
После миграции конкретного endpoint:
Наиболее эффективный подход — перенос по feature-модулям.
Пример:
features/
users/
posts/
comments/
Каждый модуль мигрируется независимо.
Иногда API слой огромен:
api/
usersApi.js
postsApi.js
authApi.js
commentsApi.js
Необязательно переписывать всё.
RTK Query может постепенно оборачивать существующие сервисы.
Большинство старых проектов уже имеют:
RTK Query не требует переписывания auth-системы.
const baseQuery = fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
if (token) {
headers.se t('authorization', `Bearer ${token}`)
}
return headers
},
})
const axiosBaseQuery =
({ baseUrl }) =>
async ({ url, method, data, params }) => {
try {
const result = await axios({
url: baseUrl + url,
method,
data,
params,
})
return { data: result.data }
} catch (axiosError) {
return {
error: {
status: axiosError.response?.status,
data: axiosError.response?.data,
},
}
}
}
export const api = createApi({
reducerPath: 'api',
baseQuery: axiosBaseQuery({
baseUrl: '/api',
}),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => ({
url: '/posts',
method: 'GET',
}),
}),
}),
})
Это особенно полезно при миграции больших enterprise-проектов.
В старых проектах типизация часто строится вручную.
RTK Query уменьшает объём типов.
interface PostsState {
items: Post[]
loading: boolean
error: string | null
}
getPosts: builder.query<Post[], void>({
query: () => '/posts',
})
После миграции обычно исчезают:
Самая опасная стратегия.
Слишком высокий риск.
Может вызвать регрессии.
Нельзя одновременно использовать:
Добавление RTK Query в Store.
Создание первого API Slice.
Перенос read-only запросов.
Удаление legacy reducer.
Перенос mutation.
Перенос invalidation.
Удаление thunk/saga.
Очистка архитектуры.
После внедрения RTK Query обычно наблюдаются: