Hey!
Before we start — let me show you a situation.
You are building a medium-sized React app. You have a logged-in user. Their name shows in the Navbar. Their profile shows on the Profile page. Their orders show on the Orders page.
All three need the same user data.
So you put the user in the top-level component and pass it down through props.
It works. But then your app grows.
More components need the user. More props get passed. More levels get added. You already learned about useContext — and yes, it helps.
But now imagine this.
Your app has a shopping cart. A notification system. A theme setting. A logged-in user. All of this is global data. All of it needs to be accessible from many places. All of it can change at any time.
You have multiple contexts. Multiple providers wrapping each other. Managing all these state changes becomes messy. Debugging becomes harder. "Which context changed and why?" becomes a real question.
That is the problem Redux solves.
1. What Is Redux?
Redux is a state management library.
It gives you one central place to store all your app's data. One single source of truth. Every component reads from it. Every component updates through it.
Think of it like a government database.
Every department — health, transport, education — does not keep its own separate records. They all read from and write to one central database. One source. Always accurate. Always in sync.
That central database is your Redux store.
2. The Three Core Ideas in Redux
Redux is built on three simple ideas. Understand these and everything else makes sense.
Store — the single source of truth
The store holds your entire app's state in one object.
{
user: { name: "Ravi", role: "admin" },
cart: { items: [], total: 0 },
theme: "dark"
}
One object. Your whole app's state. Right there.
Action — describing what happened
An action is a plain object that describes what event just happened.
{ type: "ADD_TO_CART", payload: { id: 1, name: "Laptop", price: 50000 } }
{ type: "REMOVE_FROM_CART", payload: 1 }
{ type: "LOGOUT_USER" }
type — what happened.
payload — any extra data needed.
You never change the store directly. You send an action and say "this happened."
Reducer — deciding what changes
The reducer is a function that takes the current state and an action — and returns the new state.
function cartReducer(state = { items: [] }, action) {
switch (action.type) {
case "ADD_TO_CART":
return { items: [...state.items, action.payload] };
case "REMOVE_FROM_CART":
return { items: state.items.filter(item => item.id !== action.payload) };
default:
return state;
}
}
Sound familiar? Yes — same idea as useReducer. Redux takes this concept and makes it work at the app level.
3. The Redux Flow — How It All Connects
This is the part most beginners find confusing. Let us make it crystal clear.
User does something
↓
Component dispatches an Action
↓
Reducer receives Action + current State
↓
Reducer returns new State
↓
Store updates
↓
All components that need that data re-render
One direction. Always. No shortcuts.
This is called unidirectional data flow. And it is what makes Redux so predictable and easy to debug.
Quick question for you.
Why do you think having one direction for data flow makes debugging easier?
Because you always know where to look. Something went wrong? Check what action was dispatched. Check what the reducer did with it. The trail is always clear. No guessing which component updated which state.
4. Setting Up Redux in a React App
Modern Redux uses Redux Toolkit — the official recommended way. It removes a lot of the old boilerplate.
Install it:
npm install @reduxjs/toolkit react-redux
5. A Simple Example — Counter With Redux
Let us build the simplest possible Redux example. A counter.
Step 1 — Create a slice
A slice is a piece of your Redux store. It holds the state and the reducers for one feature.
// store/counterSlice.js
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: { count: 0 },
reducers: {
increment(state) {
state.count += 1;
},
decrement(state) {
state.count -= 1;
},
reset(state) {
state.count = 0;
}
}
});
export const { increment, decrement, reset } = counterSlice.actions;
export default counterSlice.reducer;
createSlice handles the action creators and reducer for you. No need to write them separately.
Step 2 — Create the store
// store/store.js
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";
const store = configureStore({
reducer: {
counter: counterReducer
}
});
export default store;
Step 3 — Provide the store to your app
// main.jsx
import { Provider } from "react-redux";
import store from "./store/store";
ReactDOM.createRoot(document.getElementById("root")).render(
<Provider store={store}>
<App />
</Provider>
);
Provider makes the store available to every component inside it. Just like Context Provider — but for Redux.
Step 4 — Use it in a component
// Counter.jsx
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement, reset } from "./store/counterSlice";
function Counter() {
const count = useSelector(state => state.counter.count);
const dispatch = useDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(increment())}>+1</button>
<button onClick={() => dispatch(decrement())}>-1</button>
<button onClick={() => dispatch(reset())}>Reset</button>
</div>
);
}
useSelector — reads data from the store. Like saying "give me this piece of the store."
useDispatch — gives you the dispatch function. You use it to send actions to the store.
Quick question for you.
Can you use useSelector in two different components — and both will always show the same up-to-date value?
Yes. Because both are reading from the same store. When the store updates — both components re-render with the latest value automatically. That is the power of a single source of truth.
6. What Does the Full Flow Look Like?
Let us trace through clicking the +1 button:
User clicks +1
↓
dispatch(increment()) fires
↓
Redux sends action { type: "counter/increment" } to the reducer
↓
Reducer runs: state.count += 1
↓
Store updates: count is now 1
↓
Counter component re-renders
↓
Screen shows: Count: 1
Every step is visible. Every step is traceable.
7. useContext vs Redux — Which One to Use?
This is the question everyone asks. Both manage global state. So when do you use which?
| useContext | Redux | |
|---|---|---|
| Best for | Simple global data — theme, user, language | Complex state with many interactions |
| Setup | Very simple | More setup needed |
| Dev tools | No | Yes — powerful Redux DevTools |
| Scalability | Gets messy at large scale | Built for large apps |
| When state changes | All context consumers re-render | Only components that use that slice re-render |
Simple rule.
Small app with simple global state — useContext is enough.
Large app with complex state, many features, many interactions — Redux is worth it.
Do not reach for Redux on day one. Start simple. Add Redux when you actually feel the pain of managing state without it.
8. Why Redux Is Still Worth Learning
You might be thinking — "this feels like a lot of setup for a counter."
Fair point. For a counter — it is overkill.
But Redux is used in large production apps for real reasons.
Redux DevTools let you time-travel through every state change. You can replay actions, inspect what changed, and debug issues in seconds.
Every state change has a clear action and a clear trail. No mystery updates. No wondering which component changed what.
Teams working on the same codebase can follow the same pattern. Everyone knows where the state is and how it changes.
Quick Summary — 5 Things to Remember
Redux is a central store for all your app state — one place, one source of truth, every component reads from it.
Actions describe what happened — you never change the store directly. You dispatch an action and let the reducer handle it.
Reducers decide the new state — they take the current state and the action, and return what the state should look like next.
useSelector reads from the store. useDispatch sends actions — these two hooks are how your components talk to Redux.
Use Redux when your app is actually complex — for simple apps, useContext is enough. Redux shines when state management becomes genuinely hard.
Redux felt intimidating when it first came out. The old way had a lot of boilerplate. Redux Toolkit fixed most of that.
Today — setting up Redux is straightforward. And once you see the DevTools in action, time-travelling through state changes, you will understand why large teams rely on it.
Try the counter example. Add a new slice for a theme toggle. See how two completely separate slices live in the same store and work independently.
That is the moment Redux starts to make sense.
If you have a question — drop it in the comments below.
Thanks for reading. Keep building.
Top comments (0)