← All articles
ReduxState ManagementFundamentalsReact Basics

What Is Redux in React JS? A Practical Guide (With a Real Example)

What is Redux in React JS, really? Here's a clear explanation of the store, actions, reducers, and dispatch — plus a working counter app with Redux Toolkit, and when you actually need it versus useState or Context.

Kumar Astik· MERN Developer13 min read

If you've searched "what is Redux in React JS" and come away more confused than when you started, you're not alone. Most explanations jump straight into store configuration and reducer boilerplate before answering the actual question anyone new to Redux is asking: what problem does this thing solve, and do I even need it? Let's answer that properly, in order, with real code instead of abstractions.

Short definition first, since you came here for one: Redux is a predictable state container for JavaScript applications. It's not a React feature — it's a standalone library that happens to pair extremely well with React through a separate binding package called react-redux. It centralizes your application's state into a single store, and it enforces a strict, traceable process for how that state is allowed to change.

The Problem Redux Solves: Prop Drilling

Redux only makes sense once you've felt the problem it fixes, so start there. Imagine a component tree where a user's logged-in status lives at the top, in App, but a deeply nested button four levels down needs to know whether to show "Log In" or "Log Out." React's normal answer is props — pass the value down from parent to child. That's fine for one level. It gets ugly fast past three or four.

function App() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  return <Dashboard isLoggedIn={isLoggedIn} setIsLoggedIn={setIsLoggedIn} />;
}

function Dashboard({ isLoggedIn, setIsLoggedIn }) {
  // Dashboard doesn't use isLoggedIn at all — it just forwards it
  return <Header isLoggedIn={isLoggedIn} setIsLoggedIn={setIsLoggedIn} />;
}

function Header({ isLoggedIn, setIsLoggedIn }) {
  // Neither does Header
  return <NavBar isLoggedIn={isLoggedIn} setIsLoggedIn={setIsLoggedIn} />;
}

function NavBar({ isLoggedIn, setIsLoggedIn }) {
  // Finally, four levels down, someone actually uses it
  return (
    <button onClick={() => setIsLoggedIn(!isLoggedIn)}>
      {isLoggedIn ? "Log Out" : "Log In"}
    </button>
  );
}

Dashboard and Header don't care about isLoggedIn. They're just relaying it because there's no other path for the data to travel. This is prop drilling — passing state through components that don't use it, purely so a component further down the tree can reach it. In a small app it's a minor annoyance. In a real app with dozens of components and a dozen pieces of shared state, it turns every refactor into a chore, because moving one component means rewiring every prop chain that fed it.

Redux fixes this by giving every component — no matter how deeply nested — direct access to shared state, without a single prop being passed through components that don't need it. NavBar reads isLoggedIn straight from the store. Dashboard and Header don't need to know it exists.

It's worth being honest about scale here, because this is where a lot of Redux explanations oversell the problem. A three-level prop chain for one boolean isn't really worth reaching for a state management library over — React's Context API can solve that specific case just fine, and with far less setup. Redux starts pulling ahead once you have many pieces of shared state, updated from many different places, read by components scattered all over an unrelated part of the tree. A shopping cart that gets modified from a product card, a cart icon in the header, and a checkout page all at once is a much stronger case for Redux than a single login flag ever is.

The Core Redux Concepts

Redux is built on four ideas. Understand these four and you understand Redux — everything else is implementation detail.

1. The Store

The store is a single JavaScript object that holds your entire application's shared state. There's exactly one store per app. Instead of state being scattered across dozens of components, it lives in one predictable place that any component can read from.

2. Actions

An action is a plain object that describes something that happened — never how the state should change, only what occurred. By convention it has a type field and, often, a payload carrying whatever data the change needs.

// An action describing what happened, not what to do about it
{ type: "counter/incremented", payload: 1 }
{ type: "auth/loggedIn", payload: { userId: "u_123", name: "Astik" } }

3. Reducers

A reducer is a pure function that takes the current state and an action, and returns a new state. "Pure" is doing a lot of work in that sentence: a reducer never mutates the existing state object, never makes API calls, and never produces a different result for the same inputs. Given the same state and the same action, it always returns the same new state.

function counterReducer(state = { value: 0 }, action) {
  switch (action.type) {
    case "counter/incremented":
      return { value: state.value + 1 };
    case "counter/decremented":
      return { value: state.value - 1 };
    default:
      return state;
  }
}

4. Dispatch

dispatch is the only way to trigger a state change. A component doesn't update the store directly — it dispatches an action, the store runs that action through the reducer, and the reducer decides what the new state looks like. This one-directional flow — dispatch an action, reducer computes new state, components re-render with the new state — is what makes Redux apps predictable and easy to debug. You can log every single action your app ever dispatches and replay the exact sequence that led to any bug.

Putting It Together: A Counter App With Redux Toolkit

Here's the part most explanations get outdated on. If you're learning Redux today, you should learn it through Redux Toolkit (RTK) — the official, recommended way to write Redux, and has been for several years now. The hand-written reducer-and-action-type pattern above is worth understanding conceptually, but Redux Toolkit removes almost all of the boilerplate around it. Older tutorials that skip RTK are teaching you the hard way to do something that's been simplified.

First, install what you need:

npm install @reduxjs/toolkit react-redux

Then define a slice — a bundle of state, reducers, and auto-generated actions for one feature of your app:

// counterSlice.js
import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    incremented: (state) => {
      state.value += 1; // RTK lets you "mutate" safely under the hood
    },
    decremented: (state) => {
      state.value -= 1;
    },
    incrementedByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
});

export const { incremented, decremented, incrementedByAmount } = counterSlice.actions;
export default counterSlice.reducer;

Notice createSlice generates the action creators and action types for you — incremented and decremented are ready to dispatch immediately, no manual type strings anywhere. Next, wire the slice into a store:

// store.js
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

Then make the store available to your whole app with the Provider component, typically in your entry file:

// main.jsx
import { Provider } from "react-redux";
import { store } from "./store";

<Provider store={store}>
  <App />
</Provider>

And finally, use it from any component — no prop drilling, no matter how deep the component sits:

import { useSelector, useDispatch } from "react-redux";
import { incremented, decremented } from "./counterSlice";

function Counter() {
  const count = useSelector((state) => state.counter.value);
  const dispatch = useDispatch();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch(incremented())}>+</button>
      <button onClick={() => dispatch(decremented())}>-</button>
    </div>
  );
}

useSelector reads a slice of state from the store and re-renders the component only when that specific slice changes — not on every state update, anywhere. useDispatch gives you the dispatch function to fire actions. That's the entire loop: dispatch an action, the reducer computes new state, useSelector notices and re-renders. No prop chains, no relaying components, no manual wiring past the Provider at the root.

Handling Async Logic: Thunks and RTK Query

Everything above covers synchronous updates — incrementing a counter happens instantly, with no waiting involved. Real apps need to fetch data from an API, though, and reducers can't do that themselves since they have to stay pure. This is where a lot of "what is Redux" explanations stop short, so it's worth covering properly.

The traditional answer is a thunk — a function, instead of a plain action object, that Redux lets you dispatch. A thunk can contain async logic, and it dispatches real actions once that logic resolves. Redux Toolkit sets this up for you automatically; you don't need to install anything extra to use it.

// A thunk handling an async API call
export const fetchUser = (userId) => async (dispatch) => {
  dispatch({ type: "user/loading" });
  try {
    const res = await fetch(`/api/users/${userId}`);
    const data = await res.json();
    dispatch({ type: "user/loaded", payload: data });
  } catch (err) {
    dispatch({ type: "user/failed", payload: err.message });
  }
};

// Usage in a component
const dispatch = useDispatch();
useEffect(() => {
  dispatch(fetchUser("u_123"));
}, [dispatch]);

Notice the pattern: the thunk itself isn't a reducer and doesn't touch state directly. It dispatches ordinary actions — loading, loaded, failed — and a normal reducer handles each of those the same way it would handle any other action. The async complexity lives entirely in the thunk, and the reducer stays pure and predictable.

For most new projects, Redux Toolkit's RTK Query goes a step further and removes the need to hand-write thunks for data fetching entirely — you describe an endpoint once, and RTK Query generates the hooks, loading states, and caching for you. It's worth knowing thunks exist and how they work conceptually, since you'll run into hand-written ones in existing codebases, but for new code RTK Query is usually the less-boilerplate option.

Redux DevTools: Why Debugging Redux Feels Different

One reason teams stick with Redux even after Context and other libraries became more common is the Redux DevTools browser extension. Because every state change flows through a dispatched action and a pure reducer, DevTools can record every single action your app has ever dispatched, show you the exact state diff each one produced, and let you jump back to any earlier point in that history — literally rewinding your app's state to debug an issue.

This is a direct consequence of the constraints Redux enforces — no direct mutation, no side effects in reducers, one-directional data flow. Those constraints feel restrictive at first, especially coming from useState, but they're exactly what makes this level of debugging possible. Redux Toolkit wires up DevTools support by default through configureStore, so you get this for free the moment you set up a store — no extra configuration needed.

Redux vs Context vs useState: When to Reach for Which

This is the question that actually matters day to day, and it's the one most Redux tutorials skip entirely. Here's a straightforward way to decide:

  • useState — for state that belongs to one component and doesn't need to be shared: a form input's value, whether a modal is open, a toggle's on/off state.
  • useContext — for state that's shared but changes rarely: current theme, the logged-in user's profile, a language preference. Context re-renders every consumer on any change, which is fine when updates are infrequent.
  • Redux — for state that's shared widely and changes often: a shopping cart, notifications, a complex multi-step form's data, anything read and updated from many unrelated parts of the app. Redux's selector pattern means components only re-render for the exact slice of state they subscribe to, which Context can't do on its own.

A useful gut check: if you're reaching for Redux because "the app might get big later," that's usually premature. If you're reaching for it because you've actually hit prop drilling three or four levels deep, or because Context re-renders are causing a measurable performance problem, that's Redux earning its place rather than being installed out of habit.

Do You Actually Need Redux?

Here's the honest answer nobody selling a Redux course wants to give you: probably not, for your first several projects. React's built-in useState and useContext cover a large share of real applications without any extra library. Redux earns its keep specifically when state is large, shared across many components that don't have a parent-child relationship, and updated frequently from many different places in your codebase — think a large dashboard, a collaborative editor, or an e-commerce cart that a dozen unrelated components need to read and modify.

If your app is a handful of pages with mostly local state and a couple of shared values like theme or auth, Context will get you there with far less setup. Reach for Redux when you can point to the actual pain — not because a tutorial told you every serious app uses it. It's genuinely fine, and common, to ship a production app that never touches Redux at all.

The flip side is also true: teams sometimes wait too long and end up bolting Redux onto an app that's already sprawling, at which point migrating dozens of components off scattered useState and prop-drilled callbacks is a much bigger job than it would have been earlier. There's no perfect moment, but a reasonable signal is the third time you catch yourself drilling the same piece of state through more than two or three components just to reach one that actually needs it.

"Redux comes with a lot of boilerplate, and believe me, many JS developers are not a big fan of this library. Think before you use it."

Common Redux Mistakes Beginners Make

  • Mutating state directly in a hand-written reducer — outside of Redux Toolkit's createSlice, reducers must return a new state object, never modify the existing one in place.
  • Putting everything in the Redux store, including state that's local to one component — this defeats the point and makes simple components harder to reason about, not easier.
  • Skipping Redux Toolkit and learning the old hand-rolled action-type-and-switch-statement pattern first — it's more boilerplate for the same result, and it's not what real teams write anymore.
  • Dispatching actions inside a reducer, or calling an API directly inside one — reducers must stay pure. Side effects belong in middleware (like RTK Query or a thunk), not the reducer itself.
  • Reaching for Redux on day one of a small project instead of starting with useState/useContext and migrating only once real prop drilling or performance pain shows up.

How to Actually Learn Redux (Not Just Read About It)

Reading this post gets you the vocabulary — store, action, reducer, dispatch — and a working counter example to reference. It won't build the instinct for when Redux is the right call versus overkill. That instinct only comes from actually hitting the problem: building something with prop drilling three levels deep, feeling the pain, and then refactoring it into a Redux store yourself to feel the difference directly.

A good first exercise: take the login-status example from earlier in this post, actually build it with plain props across four nested components, then rebuild the same thing with a Redux Toolkit slice. You'll feel exactly what problem Redux solves, instead of just being told about it — the same deliberate-practice approach that works for hooks works here too.

Want a structured way to build that instinct for React state and hooks in general? Read the React hooks deliberate practice guide →

And once you're comfortable with the concepts here, the fastest way to make them stick is to write components against real tests — not just read code someone else already got working.

Ready to practice state management for real? Try a React coding challenge →

Frequently asked questions

What is Redux in React JS?

Redux is a predictable state container for JavaScript apps. It's not actually part of React — it's a standalone library that centralizes an application's state into a single store, so any component can read or update that state through a defined, traceable process instead of passing data through props at every level.

Is Redux part of React?

No. Redux is a separate JavaScript library that works with any framework — React, Angular, Vue, or even vanilla JS. The react-redux package is what connects Redux's store to React components specifically, through hooks like useSelector and useDispatch.

Do I need Redux for every React app?

No. Most small to medium apps are better served by useState for local state and useContext for state that's shared but changes rarely. Redux earns its complexity when state is large, shared across many unrelated components, and updated frequently from many different places.

What is Redux Toolkit and is it different from Redux?

Redux Toolkit (RTK) is the official, recommended way to write Redux logic today. It wraps the same core Redux concepts — store, actions, reducers — but removes most of the boilerplate through utilities like createSlice and configureStore. If you're learning Redux now, learn it through Redux Toolkit, not the older hand-written pattern.

What's the difference between Redux and the Context API?

Context is a built-in React feature for passing data down without prop drilling — it's not a state management library on its own. Redux is a full state management solution with a structured update pattern, devtools, and middleware support. Context re-renders every consumer on any value change; Redux (via react-redux) lets components subscribe to only the slice of state they actually use.

About the author
Kumar AstikMERN Developer
Keep reading