Clean β’ Professional
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.
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:
_20251105_072707.png&w=3840&q=75)
Redux is useful when:
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) 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 easilyInstall the required packages:
npm install @reduxjs/toolkit react-redux
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.store.js
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";
const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export default store;
configureStore() sets up:
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>
);
Use two hooks from react-redux:

useSelector() β to access state
useDispatch() β to send actions
dispatch method, 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;
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 lets you intercept actions before they reach the reducer.
Common examples include:
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().
| 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 |
Use Redux if your app:
If your app is small and state is simple, Context API or useState might be enough.
| 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 |