Start now →

The Intelligence of Redux

By Akshat Tiwari · Published April 6, 2026 · 16 min read · Source: Level Up Coding
DeFi
The Intelligence of Redux

Let’s talk about Redux.

Not about the code, but the internal working. The version where we actually ask why things are the way they are — and keep pulling on that thread until the whole system makes sense.

We’ll start from a real problem, build up the concepts one question at a time, and by the end you’ll not only know how Redux works but why it was designed this way.

The Problem: Accessibility of State

Before we talk about Redux, let’s talk about the problem it solves.

In React, when you define a state using useState, it lives inside that component. That's it. It's local. If another component needs it, you pass it down as a prop. Simple enough.

But what happens when a piece of state needs to be accessed by components that are far apart in the component tree? Say, a user’s authentication status — the navbar needs it, the sidebar needs it, the settings page needs it, and so does the checkout flow. These components might not even be in the same branch of the tree.

You could “lift the state up” — move it to a common ancestor and pass it down. But then you’re threading that state through every component in between, even components that don’t care about it. They’re just middlemen, passing props they’ll never use. This is prop drilling, and it gets messy fast.

App (state: isLoggedIn)
└── Layout (passes isLoggedIn as prop)
├── Navbar (passes isLoggedIn as prop)
│ └── UserAvatar ← actually needs it
└── Sidebar (passes isLoggedIn as prop)
└── UserMenu ← actually needs it

Layout, Navbar, and Sidebar have no business holding isLoggedIn. They're just couriers.

So the question becomes: what if we could move this state outside of the component tree altogether? Make it globally accessible, so any component can reach in and grab it directly without needing a courier chain.

That’s the primary job of Redux. It takes states that have a global presence — states that multiple, distant components care about — and stores them in a central store that lives outside React’s component tree entirely.

But First — What Even Is a State?

Before we go further into Redux internals, let’s make sure we have a clean definition of state, because everything that follows depends on it.

A state is data that can change over time, and every time it changes, the UI needs to update to reflect that change.

In React, when state changes, a re-render is triggered. React re-runs the component function, calculates the new output, and updates the DOM. That’s the contract.

This is also why a regular let variable doesn't work as state. Consider:

let count = 0;
function handleClick() {
count = count + 1;
// count is now 1 in memory, but React has no idea.
// No re-render. UI still shows 0.
}

When you do count = count + 1, the value changes in JavaScript memory. But React is completely unaware. No re-render happens. The UI doesn't update.

Now compare:

const [count, setCount] = useState(0);

function handleClick() {
setCount(count + 1);
// React is notified. Re-render happens. UI updates.
}

setCount isn't just assignment. It's a signal to React — "something changed, please recalculate." It updates the value and triggers the render cycle.

There’s also a subtle second issue with let: every time a component re-renders, the function body runs again from the top. Any let variable declared inside the component gets reset to its initial value on every render. So even if you somehow triggered a re-render, your let counter would start from zero again anyway.

State, on the other hand, is stored by React outside your component function and handed back to you each render. That’s how it persists.

So to summarize: state = data + the mechanism to notify React when it changes + persistence across renders. A let variable only has the first one.

Okay, Back to Redux. What Happens When a Redux State Changes?

Since we’ve established what state is, we can ask the right question: what should happen when a Redux state changes?

The answer follows directly from the definition. A re-render needs to happen — specifically in the components that use that state.

Now, Redux is a global store. Our entire app is wrapped inside it. So when a state in the Redux store changes, what exactly re-renders?

Let’s make this concrete with an example.

Say we have a React app with two global states: x and y. Four components use x, and six components use y. The whole app is wrapped in a Redux Provider.

When state x changes, which components re-render?

Option 1: The whole app re-renders because it’s wrapped inside Redux.

Option 2: All 10 components using any Redux state re-render.

Option 3: Only the 4 components that use x re-render.

The answer is Option 3. Only the components subscribed to x re-render.

That’s impressively surgical. So now we have to ask — how does Redux know which components are using x? And how does React know to re-render only those 4?

Enter react-redux: The Bridge

Quick but important detour. Redux, by itself, is not a React thing. It’s a standalone state management library. You can use it with Vue, Angular, vanilla JavaScript — whatever. Redux doesn’t know or care about React.

To use Redux with React, you need a separate library: react-redux. It’s the bridge that connects Redux’s store to React’s rendering system.

react-redux gives you two main hooks:

These two hooks are what make the smart re-rendering possible. Let’s look at how.

useSelector: More Than Just a Getter

When a component wants to read a Redux state, it does something like this:

const x = useSelector(state => state.x);

On the surface, this looks like a simple getter. But useSelector is doing something much more significant behind the scenes.

When a component calls useSelector, it's not just reading a value — it's subscribing to that slice of state. Redux internally registers this component as an interested party in state.x. If state.x ever changes, Redux knows this component needs to be notified.

This is the subscription model. Think of it like a newsletter. The components that call useSelector(state => state.x) are signed up for the "x changed" newsletter. The ones that only use useSelector(state => state.y) are on a completely different list. The two are independent.

So when x changes, Redux looks at its subscriber list for x, finds the 4 components that subscribed, and triggers a re-render for each of them. The 6 components subscribed to y don't even hear about it.

That’s how the surgical re-rendering works — not magic, just subscriptions.

But How Does useSelector Decide to Re-render?

Here’s a detail worth understanding deeply, especially if you ever hit performance issues with Redux.

After every state change, useSelector re-runs your selector function and compares the new result to the previous result. If they're the same, no re-render. If they're different, re-render.

The comparison is a strict equality check (===). This means:

// This is fine — primitives compare by value
useSelector(state => state.count)

// This causes a problem - new object created every time
useSelector(state => ({ x: state.x, y: state.y }))

In the second example, a new object {} is created every time the selector runs, even if x and y haven't changed. Since {} !== {} in JavaScript (objects compare by reference, not value), useSelector thinks something changed and triggers a re-render unnecessarily.

This is a common footgun. If you need to select multiple pieces of state, either use separate useSelector calls, or use a memoized selector (libraries like reselect help with this).

How Does State Actually Change in Redux?

We know how state is read (useSelector). Now let's talk about how it's changed.

In regular React, you call the updater function from useState. In Redux, the equivalent mechanism involves three concepts working together: the store, actions, and reducers.

Let’s define each one.

The Redux Store

The store is the single central object that holds all your global state. Every Redux state — x, y, and whatever else — lives here. Think of it as a plain JavaScript object that Redux manages on your behalf.

There’s only ever one store in a Redux application. This is intentional — having a single source of truth means your state is predictable and debuggable. At any point in time, you can look at the store and see the complete state of your entire app.

Actions

An action is a plain JavaScript object that describes a change you want to make to the state. It always has a type field that identifies what kind of change it is, and optionally carries additional data.

// An action describing "increment x by 5"
{ type: "increment", incrementBy: 5 }
// An action describing "log the user out"
{ type: "logout" }
// An action describing "add an item to the cart"
{ type: "addToCart", item: { id: 42, name: "Red Shoes", price: 999 } }

An action is just data. It doesn’t do anything by itself. It’s a description of intent — a note that says “I would like this to happen.”

Reducers

A reducer is a function that takes the current state and an action, and returns the new state. It’s where the actual logic of how the state changes lives.

function counterReducer(state = { value: 0 }, action) {
switch (action.type) {
case "increment":
return {
...state,
value: state.value + action.incrementBy
};
case "decrement":
return {
...state,
value: state.value - action.incrementBy
};
default:
return state;
}
}

Notice a few things:

  1. The reducer receives the current state and an action — both are inputs.
  2. It returns a new state object — it never mutates the existing one. That ...state spread creates a copy first, then overrides specific fields.
  3. It handles different action types using a switch statement.
  4. If the action type is unknown, it just returns the state unchanged.

Reducers are supposed to be pure functions — given the same inputs, they always return the same output, with no side effects. No API calls, no random numbers, no logging. Just data in, data out. This purity is what makes Redux predictable and testable.

Dispatch: The Trigger

So how do you actually fire off a state change? You dispatch an action.

const dispatch = useDispatch();

function handleClick() {
dispatch({ type: "increment", incrementBy: 5 });
}

When you call dispatch, you're sending that action object to the Redux store. Redux passes it to the relevant reducer, which calculates the new state. The store updates. Subscribed components are notified. Re-renders happen.

One terminology point worth being precise about: you dispatch an action, not a reducer. The reducer is an internal handler — you never call it directly. It handles the action behind the scenes. The public interface is dispatch + action. The reducer is the implementation detail.

The Full Redux Data Flow

Let’s put it all together.

User Event (click, input, etc.)

dispatch(action)

Reducer receives (currentState, action)

Reducer returns newState

Store updates

useSelector runs equality check on all subscribers

Only components whose selected value changed → re-render

This is unidirectional data flow, and it’s one of the core design wins of Redux. State only ever changes through this pipeline. There’s no backdoor. You can’t accidentally mutate state from a random part of the codebase.

This predictability pays dividends when debugging. If something went wrong with your state, you look at the dispatched actions and the reducers. That’s the entire surface area. The complete history of state changes in your app is just a trace of actions that were dispatched — which brings us to one of Redux’s most powerful features.

Redux DevTools: Time-Travel Debugging

Because every state change flows through dispatch → reducer, Redux can record a log of every action ever dispatched. That’s not just useful — it unlocks something called time-travel debugging.

The Redux DevTools browser extension lets you:

This is only possible because of the strict unidirectional flow. If state could be mutated from anywhere at any time, there’d be no audit trail. The architectural choice of actions + reducers isn’t just about organization — it’s what makes this level of debuggability possible.

Modern Redux: Redux Toolkit (RTK)

Everything we’ve covered so far is “classic” Redux. And while those concepts are essential to understand, the syntax you’ll actually write day-to-day looks different in modern Redux.

The Redux team now strongly recommends Redux Toolkit (RTK), which was created to address one of Redux’s biggest complaints: too much boilerplate.

In classic Redux, setting up a single piece of state required:

Three separate moving parts, all for one feature. RTK’s createSlice bundles all three into one:

import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
increment: (state, action) => {
state.value += action.payload.incrementBy;
// Note: RTK uses Immer internally, so you CAN write mutating syntax here.
// It's converted to an immutable update behind the scenes.
},
decrement: (state, action) => {
state.value -= action.payload.incrementBy;
}
}
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

And dispatching becomes:

dispatch(increment({ incrementBy: 5 }));

increment here is an auto-generated action creator — a function that returns the correct action object with the right type string. You don't write { type: "counter/increment", payload: {...} } manually anymore; RTK generates and manages that for you.

One more thing worth noting: RTK uses a library called Immer under the hood. That’s why inside a createSlice reducer you can write state.value += 5 directly — it looks like mutation, but Immer intercepts it and produces a proper immutable update. You get cleaner code without sacrificing the immutability guarantee.

If you’re starting a new project with Redux today, RTK is the default. The concepts are identical — store, actions, reducers, dispatch — the syntax is just less painful.

Redux vs Context API: The Obvious Question

At this point, someone always asks: “Why not just use React’s built-in Context API? It also makes state globally accessible.”

Fair question. Context does solve the prop drilling problem. But it has a meaningful difference in how re-renders work.

With Context, when the context value changes, every component that consumes that context re-renders — regardless of whether they care about the specific piece of data that changed. There’s no equivalent of useSelector's granular subscription. It's all or nothing.

So if you have a context object with 10 properties, and only one of them changes, all consumers re-render anyway. For small apps or infrequently-changing data, this is fine. For larger apps with frequently-updating global state, it can become a performance problem.

Redux, as we’ve seen, is surgical. Only components subscribed to the specific state that changed will re-render.

Beyond performance, Redux also gives you:

The honest answer to “Redux vs Context” is: Context is great for simpler, slower-changing globals — theme, locale, current user. Redux earns its weight when you have complex, frequently-updated state across a large app with a team that needs predictability and good debugging tools.

Putting It All Together: A Mental Model

Let’s close with a mental model that ties everything together.

Imagine the Redux store as a whiteboard in the center of a room. Components can walk up to the whiteboard and read from it using useSelector. When they first read from it, they also sign their name on a sticky note next to the value they care about — that's the subscription.

When a component wants to change something on the whiteboard, it can’t just walk up and erase it directly. It has to write a note describing the change it wants (that’s the action) and hand it to the designated whiteboard manager (the reducer). The manager reads the note, makes the authorized change, and everyone whose sticky note is on an affected value gets notified and updates their view.

No one can sneak up and scribble on the whiteboard outside this process. Everything goes through the manager. And every note ever handed in is saved — that’s your action log, and it’s what makes time-travel debugging possible.

That’s the system design of Redux.

Quick Terminology Recap

Summary

Let’s zoom out and trace the full narrative.

React’s local state is powerful, but it doesn’t scale well when the same data needs to be accessible across many distant components. Prop drilling is the symptom; global state management is the cure.

Redux solves this by moving shared state into a central store that lives outside the component tree. Any component can read from it using useSelector, which also sets up a subscription — so that component re-renders when its relevant state changes, and only when that state changes. This is what makes Redux efficient even at scale.

State changes happen through a strictly enforced pipeline: you dispatch an action, a reducer handles it and returns a new state, the store updates, and subscribed components are notified. Nothing bypasses this pipeline. That predictability is what enables Redux DevTools and its time-travel debugging.

Modern Redux, written with Redux Toolkit, keeps all these concepts but eliminates the boilerplate — createSlice bundles reducers and action creators, Immer handles immutability transparently.

And compared to React Context, Redux offers more granular re-renders, powerful DevTools, middleware support, and a consistent pattern that scales with your team and your codebase.

That’s Redux — not just what it does, but why every part of it is designed the way it is.

Connect with me : Linkedin


The Intelligence of Redux was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

This article was originally published on Level Up Coding and is republished here under RSS syndication for informational purposes. All rights and intellectual property remain with the original author. If you are the author and wish to have this article removed, please contact us at [email protected].

NexaPay — Accept Card Payments, Receive Crypto

No KYC · Instant Settlement · Visa, Mastercard, Apple Pay, Google Pay

Get Started →