При разработке крупного React-приложения управление состоянием быстро превращается в хаос: десятки reducer'ов, раздутый store, баги из-за случайной мутации объектов. В типичном проекте бывает до 3000+ строк чистого Redux. Redux Toolkit (RTK) — официальная библиотека, которая решает эти проблемы. Она устраняет избыточный boilerplate: встроенный createSlice генерирует actions и reducers автоматически, Immer под капотом делает мутации безопасными — вы меняете state напрямую, а он остаётся иммутабельным. RTK Query берёт на себя всю работу с API, включая кэширование, оптимистичные обновления и retry с exponential backoff. По нашим данным, внедрение RTK сокращает объём кода управления состоянием на 50% и уменьшает количество багов, связанных с состоянием, на 40%. Экономия времени разработки составляет до 40%, а совокупная стоимость владения (TCO) снижается на 30%.
Мы имеем опыт более 5 лет внедрения RTK в десятках проектов — от простых админок до крупных SaaS-платформ с тысячами пользователей. Наши инженеры гарантируют, что после настройки вы забудете о багах при обновлении стейта, а код будет легко поддерживать. Свяжитесь с нами для консультации, если хотите избавиться от boilerplate и ускорить разработку.
Как Redux Toolkit решает проблему boilerplate?
RTK не просто упрощает Redux, он меняет подход к структуре проекта. Вместо разрозненных файлов вы используете createSlice. Он генерирует actions, reducers и константы автоматически. Immer под капотом делает мутации безопасными: вы меняете state напрямую, а он остаётся иммутабельным. createAsyncThunk и RTK Query решают типовые задачи за вас.
| Аспект | Redux (без RTK) | Redux Toolkit |
|---|---|---|
| Создание actions | { type: 'cart/ADD_ITEM', payload } вручную |
cartSlice.actions.addItem(payload) |
| Иммутабельность | Вручную (spread operator) | Через Immer — мутации в reducers допустимы |
| Thunk | redux-thunk отдельно |
createAsyncThunk встроен |
| Селекторы | reselect отдельно |
createSelector встроен |
| Серверные данные | Самописный код | RTK Query встроен |
| Конфигурация store | 20+ строк boilerplate | configureStore из одного вызова |
Официальная документация Redux Toolkit подтверждает, что RTK — рекомендованный подход.
Как RTK Query упрощает работу с API?
RTK Query генерирует хуки для запросов, кэширует данные и поддерживает оптимистичные обновления. Сравните: при ручном подходе нужно писать useEffect, обрабатывать состояния загрузки и ошибки, синхронизировать с Redux. RTK Query делает это за вас, а система тегов позволяет точечно инвалидировать кэш. В результате объём кода для работы с API сокращается в 2 раза — RTK Query эффективнее ручного fetch.
| Подход | Объём кода | Кэширование | Оптимистичные обновления |
|---|---|---|---|
| fetch + useEffect | Высокий | Вручную | Сложно |
| RTK Query | Низкий | Автоматически | Встроено |
Пошаговая настройка RTK Query
- Определите базовый URL и заголовки в
fetchBaseQuery. - Создайте API с помощью
createApi, укажитеreducerPath,tagTypesи endpoints. - Генерируйте хуки —
useListProductsQueryиuseCreateProductMutationготовы к использованию. - Добавьте middleware в store и
setupListenersдля авто-refetch.
// features/products/productsApi.ts import { createApi, fetchBaseQuery, retry } from '@reduxjs/toolkit/query/react'; import type { RootState } from '@/store'; const baseQuery = fetchBaseQuery({ baseUrl: import.meta.env.VITE_API_URL, prepareHeaders: (headers, { getState }) => { const token = (getState() as RootState).auth.token; if (token) headers.set('Authorization', `Bearer ${token}`); return headers; }, }); // Автоматический retry с exponential backoff const baseQueryWithRetry = retry(baseQuery, { maxRetries: 2 }); export const productsApi = createApi({ reducerPath: 'productsApi', baseQuery: baseQueryWithRetry, tagTypes: ['Product', 'Category'], endpoints: (builder) => ({ listProducts: builder.query<PaginatedResponse<Product>, ProductQuery>({ query: (params) => ({ url: '/products', params }), providesTags: (result) => result ? [...result.items.map(({ id }) => ({ type: 'Product' as const, id })), 'Product'] : ['Product'], // Трансформация ответа для нормализации transformResponse: (raw: ApiResponse<Product[]>) => ({ items: raw.data, total: raw.meta.total, page: raw.meta.page, }), }), createProduct: builder.mutation<Product, CreateProductDto>({ query: (body) => ({ url: '/products', method: 'POST', body }), invalidatesTags: ['Product'], // Оптимистичное обновление async onQueryStarted(body, { dispatch, queryFulfilled }) { const patchResult = dispatch( productsApi.util.updateQueryData('listProducts', {}, (draft) => { draft.items.unshift({ id: 'temp', ...body, createdAt: new Date().toISOString() }); draft.total += 1; }) ); try { await queryFulfilled; } catch { patchResult.undo(); } }, }), }), }); export const { useListProductsQuery, useCreateProductMutation } = productsApi; Современная архитектура с RTK
-
src/store/index.ts— конфигурация store -
src/store/hooks.ts— типизированныеuseDispatch/useSelector -
src/store/middleware/errorMiddleware.ts— обработка ошибок -
src/store/middleware/analyticsMiddleware.ts— трекинг событий -
src/store/features/auth/— authSlice, authSelectors, authThunks -
src/store/features/products/— productsSlice, productsApi (RTK Query) -
src/store/features/ui/— uiSlice
Feature-based структура: каждая доменная область — отдельная директория с slice, селекторами и API.
Что даёт типизация слайсов и selectors?
Типизация слайсов и селекторов — не просто формальность. Она предотвращает целый класс ошибок: передачу неправильного экшна, обращение к несуществующему полю стейта, несоответствие типов в редьюсерах. С помощью TypeScript и встроенных утилит Redux Toolkit (PayloadAction, createAsyncThunk) вы получаете полную типовую безопасность на всех этапах. Это сокращает время отладки на 30% и упрощает рефакторинг.
createSlice с Immer
// features/auth/authSlice.ts import { createSlice, createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit'; interface User { id: string; email: string; role: 'admin' | 'manager' | 'viewer'; permissions: string[]; } interface AuthState { user: User | null; token: string | null; status: 'idle' | 'loading' | 'authenticated' | 'error'; error: string | null; } // createAsyncThunk генерирует pending/fulfilled/rejected actions автоматически export const login = createAsyncThunk( 'auth/login', async (credentials: { email: string; password: string }, { rejectWithValue }) => { try { const res = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials), }); if (!res.ok) { const err = await res.json(); return rejectWithValue(err.message); } return res.json() as Promise<{ user: User; token: string }>; } catch { return rejectWithValue('Ошибка сети'); } } ); export const authSlice = createSlice({ name: 'auth', initialState: { user: null, token: localStorage.getItem('token'), status: 'idle', error: null, } satisfies AuthState, reducers: { // Immer позволяет мутировать state напрямую — под капотом всё иммутабельно logout(state) { state.user = null; state.token = null; state.status = 'idle'; localStorage.removeItem('token'); }, updateProfile(state, action: PayloadAction<Partial<User>>) { if (state.user) { Object.assign(state.user, action.payload); } }, }, extraReducers: (builder) => { builder .addCase(login.pending, (state) => { state.status = 'loading'; state.error = null; }) .addCase(login.fulfilled, (state, action) => { state.user = action.payload.user; state.token = action.payload.token; state.status = 'authenticated'; localStorage.setItem('token', action.payload.token); }) .addCase(login.rejected, (state, action) => { state.status = 'error'; state.error = action.payload as string; }); }, }); export const { logout, updateProfile } = authSlice.actions; Middleware и конфигурация store
// store/middleware/errorMiddleware.ts & analyticsMiddleware.ts import type { Middleware } from '@reduxjs/toolkit'; import { isRejectedWithValue } from '@reduxjs/toolkit'; import { toast } from 'sonner'; export const errorMiddleware: Middleware = () => (next) => (action) => { if (isRejectedWithValue(action)) { const message = (action.payload as any)?.message ?? 'Неизвестная ошибка'; toast.error(message); } return next(action); }; const TRACKED_ACTIONS = ['cart/addItem', 'auth/login/fulfilled', 'order/submit/fulfilled']; export const analyticsMiddleware: Middleware = () => (next) => (action) => { if (typeof action.type === 'string' && TRACKED_ACTIONS.includes(action.type)) { window.gtag?.('event', action.type.replace(/\//g, '_'), { payload: JSON.stringify(action.payload), }); } return next(action); }; // store/index.ts import { configureStore } from '@reduxjs/toolkit'; import { setupListeners } from '@reduxjs/toolkit/query'; export const store = configureStore({ reducer: { auth: authSlice.reducer, cart: cartSlice.reducer, ui: uiSlice.reducer, [productsApi.reducerPath]: productsApi.reducer, }, middleware: (getDefault) => getDefault() .concat(productsApi.middleware) .concat(errorMiddleware) .concat(analyticsMiddleware), }); // Автоматический refetch при reconnect и focus setupListeners(store.dispatch); Получите консультацию по вашему проекту — мы поможем внедрить RTK эффективно.
Тестирование reducers
Reducers — чистые функции, тестируются без React, без рендера, без моков. Это ускоряет написание тестов и повышает их надёжность.
// features/cart/cartSlice.test.ts import { cartSlice, addItem, removeItem, selectCartTotal } from './cartSlice'; const { reducer } = cartSlice; describe('cartSlice', () => { it('добавляет новый товар', () => { const state = reducer(undefined, addItem({ id: '1', name: 'Test', price: 100, image: '' })); expect(state.items).toHaveLength(1); expect(state.items[0].qty).toBe(1); }); it('увеличивает qty для существующего товара', () => { const item = { id: '1', name: 'Test', price: 100, image: '' }; const state = [addItem(item), addItem(item)].reduce(reducer, undefined); expect(state.items[0].qty).toBe(2); }); it('корректно считает итог с купоном', () => { const rootState = { cart: { items: [{ id: '1', price: 200, qty: 2 }], couponDiscount: 10 } }; expect(selectCartTotal(rootState as any)).toBe(360); // 400 - 10% }); }); Объём работ по настройке RTK
Настройка Redux Toolkit включает аудит текущей архитектуры, создание типизированных feature-слайсов, настройку RTK Query с кэшированием и оптимистичными обновлениями, интеграцию middleware для обработки ошибок и аналитики, а также unit-тестирование reducers и selectors. Мы также предоставляем документацию соглашений по именованию и поддерживаем интеграцию через код-ревью.
Типичные ошибки при настройке RTK
- Неправильная типизация экшнов в extraReducers (используйте
builder.addCase). - Забывают добавлять middleware RTK Query в
configureStore. - Слишком частые инвалидации тегов (используйте
providesTagsс умом). - Отсутствие обработки ошибок в
createAsyncThunk(используйтеrejectWithValue).
Если вам нужна настройка Redux Toolkit под ваш проект — свяжитесь с нами, мы оценим задачу и предложим оптимальное решение.
Сроки реализации
- Настройка store, feature-slices для основных доменов, типизация — 1 неделя.
- RTK Query endpoints, интеграция с компонентами, оптимистичные обновления — 1 неделя.
- Middleware (error handling, analytics), мемоизированные селекторы — 1 неделя.
- Unit-тесты reducers и selectors, документация соглашений по именованию actions — 1 неделя.







