Learn Redux Toolkit (RTK) — the official, modern way to manage global state in React, with minimal boilerplate and clear structure.
1️⃣ Introduction — What is Redux?
Redux is a predictable state container for JavaScript applications.
It helps manage global state — shared data across multiple components — in a centralized store.
You can use it with React, Vue, Svelte, or even plain JavaScript.
🔹 Why use Redux (over Context API)?
| Feature | Context API | Redux Toolkit |
|---|---|---|
| Purpose | Pass data down the component tree | Manage global, shared state predictably |
| Scalability | Good for small/medium apps | Ideal for complex, multi-team apps |
| Performance | Can cause unnecessary re-renders | Optimized updates using selectors |
| Debugging | No time travel/debug tools | Redux DevTools (history, diffs, time travel) |
| Structure | Unopinionated | Enforces clear modular structure |
💡 Use Redux when your app needs:
- Complex state logic (user, theme, notifications, API data)
- Predictable updates and debugging
- Collaboration across large teams
- Middleware or async logic (API handling)
🔹 Core Benefits
- Single source of truth (one global store)
- Predictable state updates (reducers)
- Middleware for side effects
- DevTools integration
- Code clarity and scalability
2️⃣ Installation & Setup
Install both Redux Toolkit and React bindings:
npm install @reduxjs/toolkit react-redux
-
@reduxjs/toolkit→ Redux + helper APIs + middleware -
react-redux→ Connects Redux to React (Provider, hooks)
🧱 Basic Setup
store.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'
export const store = configureStore({
reducer: {
counter: counterReducer,
},
})
counterSlice.js
import { createSlice } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1 },
decrement: (state) => { state.value -= 1 },
incrementByAmount: (state, action) => { state.value += action.payload },
},
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
main.jsx
import { Provider } from 'react-redux'
import { store } from './store'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
)
3️⃣ Redux Fundamentals
| Term | Description |
|---|---|
| State | The data managed globally by Redux |
| Action | Object describing what happened |
| Reducer | Pure function that updates the state based on the action |
| Dispatch | Method to send (dispatch) actions to reducers |
| Slice | Modular unit combining state + reducers + actions (via createSlice) |
🔹 Actions & Action Creators
An action is a plain object describing a change:
{
type: 'counter/increment',
payload: 5 // optional
}
-
type→ uniquely identifies the operation -
payload→ carries data to update the state
When you call an action creator (like incrementByAmount(5)),
it returns this action object:
incrementByAmount(5)
// → { type: "counter/incrementByAmount", payload: 5 }
This is then passed to dispatch(action).
So:
dispatch(incrementByAmount(5))
➡ Sends { type, payload } to the store → reducer with matching type runs → state updates → subscribed components re-render.
4️⃣ Accessing State — useSelector & createSelector
🔹 useSelector
Read store data inside a component:
const count = useSelector((state) => state.counter.value)
🔹 Why Selectors Matter
Selectors are crucial for performance:
- React components re-render whenever the selected part of the store changes.
- Using specific selectors ensures only relevant components re-render.
🔹 Optimizing with createSelector
When your selector performs calculations (like filtering or summing),
use createSelector from @reduxjs/toolkit for memoization:
import { createSelector } from '@reduxjs/toolkit'
const selectItems = (state) => state.cart.items
export const selectTotalPrice = createSelector(
[selectItems],
(items) => items.reduce((sum, i) => sum + i.price, 0)
)
Without memoization:
- Every render recalculates derived values.
- All components using it may re-render unnecessarily.
With memoization:
- Only recomputes when
cart.itemsactually changes. - Prevents needless re-renders, improving performance.
5️⃣ Updating the Store — Dispatch Explained
🔹 What is dispatch?
dispatch sends an action object to the Redux store.
const dispatch = useDispatch()
dispatch(incrementByAmount(5))
Here:
-
incrementByAmount(5)returns{ type: 'counter/incrementByAmount', payload: 5 }. - Redux routes it to the matching reducer.
- Reducer updates state immutably (via Immer).
- Store notifies all subscribed components.
- Components that depend on changed state re-render.
Components are subscribed via
useSelector— when the selected slice changes, React triggers re-render only for those.
6️⃣ Async Logic — Thunks and Code Clarity
Redux Toolkit includes redux-thunk by default,
allowing async logic inside action creators.
🔹 Basic Example:
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
export const fetchUser = createAsyncThunk('user/fetch', async (userId) => {
const res = await fetch(`/api/user/${userId}`)
return res.json()
})
This thunk returns a function that:
- Dispatches pending action →
{ type: "user/fetch/pending" } - Waits for async work
- Dispatches fulfilled or rejected action automatically
🔹 Integrated with Slice:
const userSlice = createSlice({
name: 'user',
initialState: { data: null, loading: false },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => { state.loading = true })
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = false
state.data = action.payload
})
.addCase(fetchUser.rejected, (state) => { state.loading = false })
},
})
🔹 Why Thunks Improve Code Clarity
Without thunks:
dispatch(setLoading(true))
dispatch(fetchUserData())
dispatch(setLoading(false))
With thunks:
- Logic is centralized inside one async function.
- Reduces number of dispatch calls.
- Keeps components clean (only one
dispatch(fetchUser())).
7️⃣ Middlewares & DevTools
🔹 Middleware
Middleware intercepts actions before they reach reducers.
Useful for logging, API handling, analytics, etc.
Common ones:
-
redux-thunk(async logic) -
redux-logger(logs actions) -
redux-saga(complex side effects)
RTK includes Thunk by default.
🔹 DevTools
Redux DevTools offers:
- Action timeline (who changed what)
- State diff inspection
- Time travel debugging
Enabled automatically with configureStore() — no setup needed.
✅ Summary — Why Redux Toolkit Rocks
| Concept | Modern Redux Advantage |
|---|---|
| Setup |
configureStore() (built-in middleware + devtools) |
| State | Modular slices with createSlice()
|
| Actions | Auto-generated with types & creators |
| Async | Simple with createAsyncThunk()
|
| Performance | Optimized via selectors & memoization |
| Debugging | Integrated DevTools |
| Scalability | Structured for large codebases |
🧭 Final Takeaway
Redux Toolkit makes state management:
- Predictable (strict flow)
- Scalable (slices)
- Readable (less boilerplate)
- Performant (memoized selectors)
Whether you’re building a small app or an enterprise dashboard —
Redux Toolkit gives you a clean, maintainable way to manage state.
Code sandbox example :
💡 Pro Tip:
If you’re starting fresh with React, skip classic Redux —
go straight to Redux Toolkit (RTK). It’s simpler, faster, and officially recommended.
Top comments (0)