As React apps grow, passing data down through many layers of components ā prop drilling ā becomes unmanageable. Redux solves this by introducing a single global store that any component can read from directly, built around three core pillars: the store, actions, and reducers.
1State Management Chaos
As a React app grows, a common problem emerges: a piece of state defined in one component, like a logged-in user's data in a Header, is needed by a component many levels deeper, like an Avatar inside a Sidebar. Passing that data down through every intermediate component as props ā even ones that don't use it themselves ā is called prop drilling.
Prop drilling makes the codebase fragile: adding or renaming a piece of state means touching every intermediate component along the chain, which quickly becomes unmanageable in a large app.
// Example
console.log("Running React...");Prop Drilling ā
Passing data through components that don't need it.
2The Redux Solution
Redux solves prop drilling by moving state out of the component tree entirely and into a single global store ā one central JavaScript object that lives outside the UI hierarchy. Any component, regardless of how deeply nested it is, can subscribe directly to the store and read exactly the data it needs.
This completely bypasses the intermediate components that prop drilling would otherwise force data through, since a component no longer needs its parents to forward anything on its behalf.
import { createStore } from 'redux';
const store = createStore(reducer);Global State ā
Any component can access data directly.
3The Three Pillars
To keep a massive application predictable, Redux doesn't let you modify the store freely ā the entire architecture is built on three core pieces working together: the Store, which holds all the data; Actions, plain objects that describe what happened; and Reducers, functions that calculate what the new state should be.
Every state change in a Redux app flows through these same three pieces, which is what keeps updates traceable even as the application grows large.
const incrementAction = {
type: 'INCREMENT',
payload: 1
};The Trinity of Redux
4Pillar 1: The Store
The Store is Redux's single source of truth. Instead of ten different components each managing their own isolated piece of state, the store aggregates everything into one JavaScript object tree that lives in one place.
Having all state centralized like this makes it far easier to debug an application, track how data changed over time, or even persist an entire session by saving the store's contents to local storage.
function counterReducer(state = 0, action) {
if (action.type === 'INCREMENT') {
return state + 1;
}
return state;
}Single Source of Truth
5Pillar 2: Actions
The store can never be mutated directly ā something like store.user = null is not allowed. The only way to trigger a state change is to dispatch an Action, which is simply a plain JavaScript object describing an event that occurred.
Every action must include a type property, conventionally an uppercase string like 'USER_LOGIN', that identifies what happened so the reducer knows how to respond to it.
// Bad ā: state.count = 1;
// Good ā
: return { ...state, count: 1 };Actions are Immutable
They just report what happened.
6Action Payloads
A type alone doesn't always capture enough information ā if the user added an item to their cart, the app needs to know which item. Actions can carry extra data in a conventional payload property, which can be a string, a number, or an entire nested object.
An action like { type: 'ADD_ITEM', payload: { id: 5, qty: 1 } } tells the reducer both what happened and exactly what data is needed to compute the resulting state.
store.dispatch({ type: 'INCREMENT' });Payloads
Carrying the data payload to the store.
7Pillar 3: Reducers
Actions describe what happened, but Reducers are what figure out how the state should actually change. A reducer is a function that takes two arguments ā the current state and the incoming action ā and returns the new state.
A typical reducer inspects action.type (often with a switch statement), computes the resulting data for that case, and returns it; for any action type it doesn't recognize, it simply returns the existing state unchanged.
// UI -> Action -> Reducer -> Store -> UIThe Logic Center
(State, Action) => NewState
8The Golden Rule of Reducers
Reducers must be pure functions, which means two things: given the same inputs, they always produce the same output, and they must never mutate the existing state object. Writing state.user = 'Bob' directly is forbidden ā instead, a reducer returns an entirely new object, typically using the spread operator: return { ...state, user: 'Bob' }.
If a reducer mutates state in place instead of returning a new object, React and Redux have no way to detect that anything changed, so the UI silently fails to re-render.
<h1>Redux Philosopher Unlocked!</h1>Never Mutate State
Always return a new copy.
9Step-by-Step Breakdown
State Management Chaos. Welcome to Redux! As your React applications grow, state management becomes your biggest challenge. Imagine a user logs in via a Header component, but a Sidebar component 10 levels deep needs that user's avatar. Passing state down through every intermediate component is called 'Prop Drilling', and it creates a fragile, unmaintainable nightmare.
The Redux Solution. Redux solves prop drilling by introducing a 'Global Store'. Instead of state living inside individual components, it lives in one central JavaScript object outside of the UI tree. Any component, no matter how deep, can 'subscribe' to the Store and access exactly the data it needs, completely bypassing the intermediate components.
The Three Pillars. To maintain order in a massive application, Redux enforces strict rules. You cannot just modify the Store randomly. The entire Redux architecture is built upon three core pillars: The Store (holds the data), Actions (describe what happened), and Reducers (calculate the new data). Let's explore them.
Pillar 1: The Store. The Store is the 'Single Source of Truth'. Instead of having 10 different components managing their own isolated states, the Store aggregates all state into one giant JavaScript object tree. This makes it incredibly easy to debug, track changes over time, and even save the user's session to local storage.
Pillar 2: Actions. You can NEVER mutate the Store directly (e.g., store.user = null is illegal). The only way to trigger a state change is to send an Action. An Action is just a plain JavaScript object that describes an event. Every Action MUST have a 'type' property (usually a string in all caps) describing what occurred.
Action Payloads. Sometimes 'what happened' isn't enough information. If the user added an item to the cart, the system needs to know *which* item. Actions can carry extra data, conventionally placed inside a 'payload' property. The payload can be a string, a number, or an entire nested object.
Which property is strictly REQUIRED in every Redux action object for the system to identify the event?
- āpayload
- āname
- ātype
Pillar 3: Reducers. Actions describe *what* happened, but Reducers figure out *how* the state changes. A Reducer is a function that takes two arguments: the CURRENT state, and the incoming ACTION. It looks at the action's type, calculates the new data, and returns the NEW state object to update the Store.
The Golden Rule of Reducers. Reducers MUST be 'Pure Functions'. This means two things: 1. For the same input, they always return the same output. 2. They CANNOT mutate the existing state. Instead of modifying state.user = 'Bob', you must return a completely new object copy using the spread operator (return { ...state, user: 'Bob' }). If you mutate state directly, React will not re-render the UI!
If a reducer wants to update the name property of the state object, which is the correct Pure Function approach?
- āstate.name = action.payload; return state;
- āreturn { ...state, name: action.payload };
The Dispatch Method. We have a Store, we have Actions, and we have Reducers. But how do we actually send an Action into the system when a user clicks a button? We use the dispatch() method provided by the store. Calling dispatch(action) is what physically fires the event into the Redux ecosystem.
The Unidirectional Flow. Let's put it all together into the famous 'Unidirectional Data Flow': 1. The User interacts with the UI. 2. The UI dispatches an Action. 3. The Store sends the Action to the Reducer. 4. The Reducer calculates and returns the new State. 5. The Store saves the new State and tells the UI to re-render. The data always flows in this one, predictable circle.
What is the correct flow of data in a Redux application?
- āStore -> UI -> Reducer -> Dispatch
- āUI -> Dispatch -> Reducer -> Store -> UI
When to use Redux?. Redux requires a lot of boilerplate code. Don't use it for everything! If state is only used in one component (like a dropdown toggle), just use useState. Only use Redux for truly global state: user authentication data, complex shopping carts, or app-wide theme settings. Overusing Redux is a common beginner mistake.
Mastery Achieved. Spectacular! You now understand the core philosophy of Redux. You've mastered the Global Store, Actions, Reducers, and the Unidirectional Data flow. You are ready to install the actual libraries and wire this up inside a real React application!
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Global State Should Not Bypass Component-Level Focus Management
Even when data lives in a Redux store instead of component state, UI changes triggered by a dispatched action (opening a dialog, removing a list item) still need to manage focus and announce changes to assistive technology just as local state changes would.
2Loading and Error States Belong in the Store Too
If async data fetching results (loading, success, error) live in Redux state, make sure components render accessible loading indicators and error messages from that state rather than leaving a blank screen while data resolves.
SEO Implications
- 1
Global Store State Isn't Present in Server-Rendered Markup by Default
A Redux store's initial state is whatever was defined when `configureStore` was set up ā if real content only appears after a client-side dispatch populates the store, that content won't exist in the HTML a crawler sees before hydration.
- 2
Centralizing State Doesn't Change What's Indexable
Moving state into Redux solves data-sharing problems between components, but it has no direct effect on SEO by itself ā content still needs to be rendered into the DOM, ideally server-side, for search engines to read it.
Best Practices
Never Mutate State Inside a Reducer
Always return a new object or array from a reducer, typically via the spread operator, e.g. `return { ...state, count: state.count + 1 }` ā mutating state directly prevents Redux from detecting the change.
Give Every Action a Clear, Consistent type
Use a descriptive, consistently formatted string like `'cart/addItem'` for every action's `type` field so reducers can reliably match on it and the action's origin is obvious when debugging.
Frequent Bugs
The UI doesn't update after an action is dispatched, even though the reducer ran.
The reducer mutated the existing state object in place instead of returning a new one. Redux and React detect changes via reference comparison, so mutating state silently breaks re-renders ā always return a new object or array.
A reducer produces inconsistent results for what looks like the same action.
The reducer isn't a pure function ā it's relying on something outside its arguments, like `Date.now()`, `Math.random()`, or an external variable. Reducers must compute the new state using only the `state` and `action` arguments they receive.
Real-World Examples
Global Store for a Shopping Cart
An e-commerce app keeps cart items in a Redux store so the item count in the navbar, the cart page, and the checkout summary all stay in sync automatically, without passing cart data through unrelated components as props.
function cartReducer(state = { items: [] }, action) {
switch (action.type) {
case 'cart/addItem':
return { ...state, items: [...state.items, action.payload] };
default:
return state;
}
}