Redux Toolkit змінює підхід до створення store, автоматизуючи найкращі практики та усуваючи багато boilerplate коду. Давайте розберемося, як це працює і чому це важливо.
До появи Redux Toolkit налаштування store вимагало значних зусиль:
// Класичний Redux - багато ручної роботи
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from './reducers'
const composeEnhancers = (typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)))
❌ Багато boilerplate
❌ Відсутність захисту
❌ Складність для новачків
compose та enhancersRedux Toolkit надає функцію configureStore, яка інкапсулює всі найкращі практики:
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({
reducer: {
// Ваші reducers
},
})
Одна функція замінює весь boilerplate і додає captures потужні можливості автоматично.
Redux Toolkit вже включає в себе Redux core, тому окремо redux встановлювати не потрібно.
npm install @reduxjs/toolkit react-redux
yarn add @reduxjs/toolkit react-redux
pnpm add @reduxjs/toolkit react-redux
react-redux потрібен для зв'язки Redux store з React компонентами через hooks та Provider.Створіть файл src/store.js (або src/app/store.js для більших проєктів).
Функція configureStore приймає об'єкт конфігурації з наступними параметрами:
| Параметр | Тип | Обов'язковий | Опис |
|---|---|---|---|
reducer | Object/Function | ✅ | Root reducer або об'єкт з slice reducers |
middleware | Function | ❌ | Функція для налаштування middleware |
devTools | Boolean/Object | ❌ | Увімкнути DevTools (за замовчуванням true у dev) |
preloadedState | Object | ❌ | Початковий state (для SSR, hydration) |
enhancers | Array/Function | ❌ | Додаткові store enhancers |
reducer. Решта параметрів для advanced use cases.import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './features/counter/counterSlice'
import userReducer from './features/user/userSlice'
export const store = configureStore({
reducer: {
counter: counterReducer,
user: userReducer,
},
})
З такою конфігурацією ваш глобальний state матиме структуру:
{
counter: { /* state з counterSlice */ },
user: { /* state з userSlice */ }
}
reducer стають ключами в глобальному state. Вибирайте їх обдумано!Middleware (проміжне програмне забезпечення) — це функції, які перехоплюють кожен dispatched action перед тим, як він досягне reducer.
configureStore автоматично додає наступні middleware:
NODE_ENV === 'production') для збереження performance.Використовуйте callback getDefaultMiddleware():
import { configureStore } from '@reduxjs/toolkit'
import logger from 'redux-logger'
import counterReducer from './features/counter/counterSlice'
export const store = configureStore({
reducer: {
counter: counterReducer,
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
})
Можна налаштувати поведінку вбудованих middleware:
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
// Вимкнути immutability check для specific шляхів
immutableCheck: {
ignoredPaths: ['items.data'],
},
// Вимкнути serializability check для певних actions
serializableCheck: {
ignoredActions: ['your/action/type'],
ignoredPaths: ['items.date'],
},
// Повністю вимкнути thunk (якщо не використовуєте)
thunk: false,
}),
Приклад створення простого logger middleware:
const loggerMiddleware = (store) => (next) => (action) => {
console.group(action.type)
console.log('Dispatching:', action)
console.log('Previous State:', store.getState())
const result = next(action)
console.log('Next State:', store.getState())
console.groupEnd()
return result
}
export const store = configureStore({
reducer: {
counter: counterReducer,
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(loggerMiddleware),
})
Redux DevTools працюють "з коробки" без налаштувань:
// DevTools автоматично підключені!
export const store = configureStore({
reducer: {
counter: counterReducer,
},
})
Для advanced функцій передайте об'єкт конфігурації:
export const store = configureStore({
reducer: {
counter: counterReducer,
},
devTools: {
// Кількість actions в історії
maxAge: 50,
// Trace для actions (показує stack trace)
trace: true,
// Приховати певні екшени
actionsBlacklist: ['SOME_NOISY_ACTION'],
// Sanitize state (приховати чутливі дані)
stateSanitizer: (state) => ({
...state,
user: state.user ? { ...state.user, password: '***HIDDEN***' } : null,
}),
},
})
// DevTools автоматично вимкнені в production
export const store = configureStore({
reducer: {
counter: counterReducer,
},
// devTools: true — за замовчуванням у dev, false у prod
})
export const store = configureStore({
reducer: {
counter: counterReducer,
},
devTools: process.env.NODE_ENV !== 'production',
})
src/
├── store.js
├── features/
│ ├── auth/
│ │ ├── authSlice.js
│ │ └── AuthForm.jsx
│ ├── todos/
│ │ ├── todosSlice.js
│ │ └── TodoList.jsx
│ └── posts/
│ ├── postsSlice.js
│ └── PostsList.jsx
import { configureStore } from '@reduxjs/toolkit'
import authReducer from './features/auth/authSlice'
import todosReducer from './features/todos/todosSlice'
import postsReducer from './features/posts/postsSlice'
export const store = configureStore({
reducer: {
auth: authReducer,
todos: todosReducer,
posts: postsReducer,
},
})
Для великих застосунків можна lazy load slices:
import { configureStore } from '@reduxjs/toolkit'
import authReducer from './features/auth/authSlice'
export const store = configureStore({
reducer: {
auth: authReducer,
// Інші reducers додадуться динамічно
},
})
// Функція для додавання reducers динамічно
export function injectReducer(key, reducer) {
store.replaceReducer({
...store.getState(),
[key]: reducer,
})
}
Використання:
// У компоненті, який lazy loads
import { lazy, Suspense } from 'react'
import { injectReducer } from './store'
const AdminPanel = lazy(async () => {
const module = await import('./features/admin/adminSlice')
injectReducer('admin', module.default)
return import('./features/admin/AdminPanel')
})
Для Server-Side Rendering передайте початковий state:
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './rootReducer'
// На сервері отримуємо дані
const preloadedState = {
user: await fetchUser(),
posts: await fetchPost(),
}
const store = configureStore({
reducer: rootReducer,
preloadedState, // Ін'єкція даних з сервера
})
// Серіалізуємо для клієнта
const html = `
<script>
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState)}
</script>
`
На клієнті:
const preloadedState = window.__PRELOADED_STATE__
delete window.__PRELOADED_STATE__
const store = configureStore({
reducer: rootReducer,
preloadedState,
})
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './features/counter/counterSlice'
export const store = configureStore({
reducer: {
counter: counterReducer,
},
})
// Експортуємо типи для використання в додатку
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
Створіть типізовані версії хуків:
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from '../store'
// Використовуйте ці хуки замість звичайних useDispatch та useSelector
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
Використання в компонентах:
import { useAppSelector, useAppDispatch } from '../hooks/redux'
function Counter() {
// Автоматичний type inference!
const count = useAppSelector((state) => state.counter.value)
const dispatch = useAppDispatch()
// ...
}
У production build RTK автоматично вимикає:
Це відбувається через process.env.NODE_ENV:
// Webpack/Vite автоматично замінять process.env.NODE_ENV на 'production'
// і dead code elimination видалить development код
export const store = configureStore({
reducer: {
counter: counterReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
// У production вимикаємо перевірки
immutableCheck: process.env.NODE_ENV !== 'production',
serializableCheck: process.env.NODE_ENV !== 'production',
}),
devTools: process.env.NODE_ENV !== 'production',
})
Оберніть додаток у <Provider>:
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { store } from './store'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
)
<Provider> робить store доступним для всіх вкладених компонентів через React Context.| Аспект | createStore (класичний) | configureStore (RTK) |
|---|---|---|
| Boilerplate | Високий (10-15 рядків) | Низький (3-5 рядків) |
| DevTools | Ручне налаштування | Автоматично |
| Thunk middleware | Ручне додавання | Included |
| Immutability check | Немає | Автоматично (dev) |
| Serializability check | Немає | Автоматично (dev) |
| TypeScript support | Потребує додаткового коду | Відмінний з коробки |
| Production оптимізації | Ручні | Автоматичні |
| Рекомендація | ❌ Застарів | ✅ Стандарт |
Проблема: Ви намагаєтеся dispatch асинхронну функцію, але thunk middleware не підключено.
// ❌ Помилка
dispatch(async () => {
const data = await fetch('/api')
})
Рішення: configureStore автоматично включає thunk. Перевірте, чи ви не вимкнули його:
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
thunk: true, // Має бути true (за замовчуванням)
}),
Проблема: Ви мутуєте state поза createSlice:
// ❌ Мутація в componentі
const handleClick = () => {
state.counter.value++ // ЗАБОРОНЕНО!
}
Рішення: Завжди dispatch actions:
// ✅ Правильно
const handleClick = () => {
dispatch(increment())
}
Проблема: Ви поклали в state non-serializable значення:
// ❌ Проблемний код
const initialState = {
lastUpdated: new Date(), // Date object
fetchPromise: fetch('/api'), // Promise
callback: () => {}, // Function
}
Рішення: Використовуйте тільки plain objects, arrays, primitives:
// ✅ Правильно
const initialState = {
lastUpdated: new Date().toISOString(), // String
isFetching: false, // Boolean
}
Або ігноруйте певні шляхи (якщо це дійсно потрібно):
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredPaths: ['form.dateField'],
},
}),
Причини:
devTools: falseРішення: Встановіть розширення та перевірте конфігурацію.
configureStore — це потужна абстракція, яка:
✅ Усуває boilerplate код
✅ Автоматично налаштовує best practices
✅ Захищає від типових помилок
✅ Оптимізує performance у production
✅ Ідеально працює з TypeScript
createStore у нових проєктах. configureStore — єдиний правильний вибір.Тепер, коли store налаштовано, давайте створимо логіку (reducers та actions) за допомогою найпотужнішого інструменту RTK.
Проблеми класичного Redux
Ми тільки що витратили кілька годин (або розділів), вивчаючи класичний Redux. Ви могли помітити певний патерн: ми пишемо дуже багато коду, щоб зробити дуже прості речі.
createSlice: Революція в Redux
Пам'ятаєте, як ми створювали окремі файли для constants, actions та reducers? Цей час минув. createSlice — це функція, яка об'єднує всю логіку feature в одному місці і робить це елегантно.