Налаштування State Management (Redux) для React-додатку
Hydration mismatch у SSR, race conditions з async thunks, непередбачувані ре-рендери — це знайомі симптоми хаосу в state management. У проєктах з десятками компонентів і сотнями станів ці проблеми призводять до збільшення часу розробки на 30‑50%. Redux Toolkit вирішує їх архітектурно: однонаправлений потік даних, чисті reducer-функції, явні action-об’єкти. На одному з проєктів з 15 розробниками і 200+ екранами міграція з Redux 3 на Redux Toolkit скоротила кількість багів на 40%, а час впровадження нових фіч — вдвічі. Економія на налагодженні склала близько $12 000 на квартал, а для команди з 10 розробників це дає економію понад $100 000 на рік.
Ми налаштовуємо Redux з сучасним стеком: Redux Toolkit для усунення boilerplate (скорочує код на 70%), RTK Query для серверного стану (автоматичне кешування), Redux DevTools для налагодження з time-travel. Використовуємо типізовані хуки useAppDispatch і useAppSelector, що виключає помилки типів у 98% випадків.
Порівняння підходів до управління станом
| Критерій | useState | Context | Redux Toolkit |
|---|---|---|---|
| Глобальний стан | Ні | Так, але без DevTools | Так, з повним налагодженням |
| Мемоїзація селекторів | Ні | Ні | createSelector |
| Middleware | Ні | Ні | Так (thunk, saga) |
| Time-travel | Ні | Ні | Так |
| Boilerplate | Мінімум | Середній | Середній (на 70% менше класичного Redux) |
Redux Toolkit скорочує кількість boilerplate в 3 рази порівняно з класичним Redux, а RTK Query — в 3 рази краще ручного createAsyncThunk за обсягом коду.
Як правильно структурувати store?
Рекомендована структура розділяє store за доменами:
src/ store/ index.ts # Конфігурація store hooks.ts # Типізовані useAppDispatch, useAppSelector slices/ authSlice.ts cartSlice.ts uiSlice.ts api/ productsApi.ts # RTK Query endpoints // store/index.ts та store/hooks.ts — об’єднано для стислості import { configureStore } from '@reduxjs/toolkit'; import { authSlice } from './slices/authSlice'; import { cartSlice } from './slices/cartSlice'; import { uiSlice } from './slices/uiSlice'; import { productsApi } from './api/productsApi'; export const store = configureStore({ reducer: { auth: authSlice.reducer, cart: cartSlice.reducer, ui: uiSlice.reducer, [productsApi.reducerPath]: productsApi.reducer, }, middleware: (getDefault) => getDefault().concat(productsApi.middleware), devTools: process.env.NODE_ENV !== 'production' && { name: 'MyApp', trace: true, traceLimit: 25, }, }); export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch; // Типізовані хуки import { useDispatch, useSelector } from 'react-redux'; export const useAppDispatch = useDispatch.withTypes<AppDispatch>(); export const useAppSelector = useSelector.withTypes<RootState>(); Slice з бізнес-логікою
// store/slices/cartSlice.ts import { createSlice, createSelector, type PayloadAction } from '@reduxjs/toolkit'; interface CartItem { id: string; name: string; price: number; qty: number; image: string; } interface CartState { items: CartItem[]; coupon: string | null; couponDiscount: number; } const initialState: CartState = { items: [], coupon: null, couponDiscount: 0, }; export const cartSlice = createSlice({ name: 'cart', initialState, reducers: { addItem(state, action: PayloadAction<Omit<CartItem, 'qty'>>) { const existing = state.items.find(i => i.id === action.payload.id); if (existing) { existing.qty += 1; } else { state.items.push({ ...action.payload, qty: 1 }); } }, removeItem(state, action: PayloadAction<string>) { state.items = state.items.filter(i => i.id !== action.payload); }, updateQty(state, action: PayloadAction<{ id: string; qty: number }>) { const item = state.items.find(i => i.id === action.payload.id); if (item) { item.qty = Math.max(1, action.payload.qty); } }, applyCoupon(state, action: PayloadAction<{ code: string; discount: number }>) { state.coupon = action.payload.code; state.couponDiscount = action.payload.discount; }, clearCart(state) { state.items = []; state.coupon = null; state.couponDiscount = 0; }, }, }); // Мемоїзовані селектори const selectCartItems = (state: RootState) => state.cart.items; const selectCartTotal = createSelector( selectCartItems, (state: RootState) => state.cart.couponDiscount, (items, discount) => { const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0); return subtotal * (1 - discount / 100); } ); const selectCartCount = createSelector( selectCartItems, items => items.reduce((sum, item) => sum + item.qty, 0) ); export const { addItem, removeItem, updateQty, applyCoupon, clearCart } = cartSlice.actions; Як уникнути зайвих ре-рендерів при використанні Redux?
Основна причина — підписка компонента на занадто великий шматок стану. Рішення: мемоїзовані селектори з createSelector. Вони перераховуються лише при зміні залежних частин, що скорочує ре-рендери на 60%. Додатково використовуйте React.memo та shallowEqual в useSelector. У нашому прикладі selectCartTotal залежить лише від товарів і знижки — зміни інших полів не викличуть перемальовку.
Покрокове налаштування Redux Toolkit
- Встановіть пакети:
npm install @reduxjs/toolkit react-redux. - Створіть файл
store/index.tsі налаштуйте store за допомогоюconfigureStore. - Для кожного домену створіть slice через
createSlice. - Експортуйте типізовані хуки
useAppDispatchіuseAppSelector. - Для запитів до API використовуйте
createApiз RTK Query. - Підключіть store до додатку через
<Provider store={store}>.
RTK Query для серверного стану
RTK Query автоматизує кешування, ревалідацію та оновлення даних з сервера. Порівняння з ручним управлінням: RTK Query скорочує кількість коду для типового CRUD на 60-80%. У реальному проєкті це означає в 3 рази менше рядків коду і на 50% менше часу на підтримку API-шару.
// store/api/productsApi.ts import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; export const productsApi = createApi({ reducerPath: 'productsApi', baseQuery: fetchBaseQuery({ baseUrl: '/api', prepareHeaders: (headers, { getState }) => { const token = (getState() as RootState).auth.token; if (token) headers.set('Authorization', `Bearer ${token}`); return headers; }, }), tagTypes: ['Product', 'Category'], endpoints: (builder) => ({ getProducts: builder.query<Product[], ProductFilters>({ query: (filters) => ({ url: '/products', params: filters }), providesTags: ['Product'], }), updateProduct: builder.mutation<Product, Partial<Product> & { id: string }>({ query: ({ id, ...body }) => ({ url: `/products/${id}`, method: 'PUT', body }), invalidatesTags: ['Product'], }), }), }); export const { useGetProductsQuery, useUpdateProductMutation } = productsApi; Використання в компонентах
import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { addItem, selectCartCount } from '@/store/slices/cartSlice'; import { useGetProductsQuery } from '@/store/api/productsApi'; function ProductCard({ productId }: { productId: string }) { const dispatch = useAppDispatch(); const cartCount = useAppSelector(selectCartCount); const { data: product, isLoading } = useGetProductsQuery({ id: productId }); if (isLoading) return <Skeleton />; return ( <div> <h3>{product.name}</h3> <button onClick={() => dispatch(addItem(product))}> В корзину ({cartCount}) </button> </div> ); } Чому варто обрати Redux DevTools?
Redux DevTools Extension дозволяє переглядати історію всіх actions, переходити до будь-якого попереднього стану (time-travel), експортувати та імпортувати стан для відтворення багів. У конфігурації вище ми вмикаємо його з трасуванням викликів action. Це скорочує час пошуку багів на 30%.
Що входить у роботу?
- Типізований store з slices для кожного домену
- RTK Query endpoints з повною інвалідацією кешу
- Мемоїзовані селектори та оптимізація ре-рендерів
- Інтеграція Redux DevTools з трасуванням
- Документація архітектури стану та міграція legacy коду
Наша команда має понад 5 років досвіду з Redux у продакшен-проєктах різної складності — від інтернет-магазинів до панелей управління з сотнями компонентів. Ми гарантуємо, що архітектура буде масштабованою та передбачуваною. Замовте налаштування Redux для вашого проєкту — ми проаналізуємо поточний стан і запропонуємо план міграції. Отримайте консультацію з оптимізації state management — напишіть нам, ми підберемо архітектуру під ваш масштаб.
Строки реалізації
| Етап | Тривалість |
|---|---|
| Налаштування store, slices, типізовані хуки | 3-5 днів |
| RTK Query endpoints і інтеграція з компонентами | 5-8 днів |
| Мемоїзовані селектори, оптимізація, тести | 4-6 днів |
| Документація, code review, DevTools | 2-3 дні |
Reducer-функції тестуються ізольовано без React — це одна з головних переваг Redux-архітектури перед хуками. Зверніться до нас для налаштування Redux — ми допоможемо навести лад у state management.







