Redux & Redux Toolkit
When your React application grows, managing state across multiple components becomes challenging. Passing props through many layers or juggling multiple contexts can make your app hard to maintain.
That’s where Redux comes in — a powerful state management library that provides a centralized store to manage and share state across your entire application.
And with Redux Toolkit (RTK) — the official, modern, and simplified way to use Redux — you can write cleaner, faster, and less boilerplate code.
What is Redux?
Redux is a predictable state container for JavaScript applications.
It helps manage your app’s global state in a single source of truth (the store) and ensures that data flow is consistent and easy to debug.
Redux is based on three core principles:
- Single Source of Truth – The entire app state is stored in one centralized store.
- State is Read-Only – State can only be changed by dispatching an action.
- Changes via Pure Functions – Reducers specify how the state changes based on actions.
Why Use Redux?
Redux is useful when:
- You need to share state between many components.
- Your app’s data flow is complex.
- You want predictable behavior and easy debugging.
- You’re building a large-scale application.
Redux Architecture Overview
Redux uses a unidirectional data flow, which means data moves in one consistent direction:
Component → Action → Reducer → Store → UI
Here’s how it works:
| Concept | Description |
|---|---|
| Store | Central place that holds the entire application state |
| Action | A plain JavaScript object that describes what you want to do |
| Reducer | A pure function that takes the current state and an action, and returns a new state |
| Dispatch | A method used to send actions to the reducer |
| Selector | A function to read or access data from the store |
Redux Toolkit (RTK) – The Modern Redux
Redux Toolkit (RTK) is the official, recommended way to write Redux logic.
It simplifies the setup process and reduces boilerplate by providing:
configureStore()– Simplified store setupcreateSlice()– Combines reducers and actions in one placecreateAsyncThunk()– Handles asynchronous logic easily- Built-in middleware like Redux Thunk for async API calls
Setting Up Redux Toolkit
Install the required packages:
npm install @reduxjs/toolkit react-redux
Creating a Redux Slice
A slice represents a single piece of your app’s state (like user, cart, or theme).
Example: 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;
},
reset: (state) => {
state.value = 0;
},
},
});
export const { increment, decrement, reset } = counterSlice.actions;
export default counterSlice.reducer;
What happens here:
createSlice()automatically creates actions and reducers.- We don’t have to write separate files for each.
- State updates are written in a mutable-looking way, but Redux Toolkit uses Immer.js under the hood to keep them immutable.
Creating a Centralized Store
store.js
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";
const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export default store;
configureStore() sets up:
- Redux DevTools integration automatically
- Default middleware for async logic and error handling
Providing the Store to React
Wrap your app with the Provider component from react-redux:
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import App from "./App";
import store from "./store";
ReactDOM.createRoot(document.getElementById("root")).render(
<Provider store={store}>
<App />
</Provider>
);
Using Redux in Components
Use two hooks from react-redux:

useSelector() – to access state
- Used to read data from the Redux store.
- It allows you to extract specific parts of the global state inside any React component.
useDispatch() – to send actions
- Used to dispatch actions to the Redux store.
- It gives you access to the store’s
dispatchmethod, allowing you to update the state.
Example: Counter.js
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement, reset } from "./counterSlice";
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<h2>Count: {count}</h2>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
<button onClick={() => dispatch(reset())}>Reset</button>
</div>
);
}
export default Counter;
Handling Asynchronous Logic with createAsyncThunk
For API calls or async operations, Redux Toolkit provides createAsyncThunk().
Example:
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
export const fetchUsers = createAsyncThunk("users/fetchUsers", async () => {
const response = await fetch("<https://jsonplaceholder.typicode.com/users>");
return await response.json();
});
const userSlice = createSlice({
name: "users",
initialState: { list: [], status: "idle" },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.status = "loading";
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = "succeeded";
state.list = action.payload;
})
.addCase(fetchUsers.rejected, (state) => {
state.status = "failed";
});
},
});
export default userSlice.reducer;
This handles loading, success, and error states — perfect for async APIs.
Middleware in Redux
Middleware lets you intercept actions before they reach the reducer.
Common examples include:
- Redux Thunk → For async actions (already built into RTK)
- Redux Logger → For debugging actions
- Custom Middleware → For analytics, caching, etc.
Example of custom middleware:
const logger = (store) => (next) => (action) => {
console.log("Dispatching:", action);
let result = next(action);
console.log("Next state:", store.getState());
return result;
};
You can add it in configureStore().
Redux Toolkit vs Traditional Redux
| Feature | Traditional Redux | Redux Toolkit |
|---|---|---|
| Setup | Manual, verbose | Easy configureStore() |
| Actions | Separate files | Auto-generated |
| Reducers | Manual | Created via createSlice() |
| Async Logic | Thunk middleware setup | createAsyncThunk() built-in |
| Boilerplate | A lot | Minimal |
| DevTools | Manual setup | Automatic |
Benefits of Redux Toolkit
- Centralized and predictable global state
- Built-in async handling
- Easier debugging and testing
- Cleaner, less repetitive code
- Full support for TypeScript
- Built-in middleware and DevTools
When to Use Redux
Use Redux if your app:
- Has shared global state across many components
- Requires complex state logic
- Needs debugging tools or predictable updates
- Handles async data fetching (like APIs)
If your app is small and state is simple, Context API or useState might be enough.
Summary Table
| Concept | Description |
|---|---|
| Store | Central place for app state |
| Slice | Combines reducer logic and actions |
| Action | Describes a state change |
| Reducer | Defines how state changes |
| Dispatch | Sends actions to reducers |
| Middleware | Handles async or side effects |
