Redux is a widely used state management library in the React ecosystem, known for its predictable state updates and unidirectional data flow. While most developers use Redux with actions, reducers, and the useSelector
/useDispatch
hooks, few dive deep into how Redux actually works under the hood.
In this blog, we’ll peel back the layers of Redux and explore its core principles, including how actions are processed, how reducers update the state, and how the store efficiently notifies subscribers. By the end, you’ll have a deeper understanding of Redux’s internals, allowing you to debug issues more effectively and even build a simplified version of Redux yourself!
Let’s dive in. 🚀
Breaking Down Redux: Core vs React Bindings
To truly understand Redux under the hood, we need to separate its two key parts:
1. Redux Toolkit (RTK) – The Core of Redux
Redux itself is a standalone state management library that works independently of React. Redux Toolkit (RTK) is the modern way to use Redux, providing a more convenient API while keeping the core principles intact. It consists of:
- Store – The centralized state container
- Reducers – Pure functions that determine how the state changes
- Actions – Descriptive objects that trigger state updates
- Middleware – Custom logic that intercepts dispatched actions
- Immer & Redux-Thunk – Built-in utilities for immutability and async logic
2. React-Redux – Connecting Redux to React
While Redux is framework-agnostic, React-Redux is the official binding library that integrates Redux with React components. It provides:
-
Provider
– Makes the Redux store accessible to the React app -
useSelector
– Fetches state from the store within a component -
useDispatch
– Allows dispatching actions from components - Context-based optimizations – Ensures efficient re-renders in React
By understanding these two parts separately, you’ll gain a clear picture of how Redux and React-Redux work together to manage state efficiently.
Redux Toolkit (RTK) – The Core of Redux
At its heart, Redux operates on the publish-subscribe (pub-sub) pattern. The core idea is simple:
- A store holds the global state.
- Dispatching an action tells Redux that something has changed.
- The store calls the appropriate reducer, which updates the state immutably.
- Once the state is updated, all subscribed listeners are notified, allowing them to take action based on the change.
This predictable data flow ensures that state updates remain centralized, immutable, and traceable.
Breaking Down the Core Redux Flow
-
Store Creation
- Redux maintains a single store that holds the application’s state.
- The store is created using
configureStore()
in Redux Toolkit orcreateStore()
in vanilla Redux.
-
Dispatching an Action
- When an event occurs (like a button click), we dispatch an action (a plain JavaScript object with a
type
property). -
Example:
store.dispatch({ type: 'counter/increment' });
- When an event occurs (like a button click), we dispatch an action (a plain JavaScript object with a
-
Reducers Process the Action
- The store passes the current state and the action to the reducer function, which returns a new state.
- Redux Toolkit simplifies this using
createSlice()
, where reducers are auto-generated. -
Example reducer:
const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: (state) => { state.value += 1; }, decrement: (state) => { state.value -= 1; } } });
-
State Updates & Listeners Get Notified
- After the state is updated, Redux notifies all subscribed components or middleware, allowing them to react to the change.
Why Immutability Matters
Redux ensures that state is never mutated directly. Instead, reducers return a new copy of the state. Redux Toolkit simplifies this by using Immer, which allows us to write "mutating" code that is safely converted to immutable updates behind the scenes.
React-Redux – Connecting Redux to React
Redux is a framework-agnostic state management library, meaning it doesn't depend on React. However, in a React application, we need a way to connect Redux to components efficiently. This is where React-Redux comes in.
At its core, React-Redux relies on React Context and React’s built-in hooks to provide seamless state management. Let’s break it down:
How Redux Store is Provided to React Components
- The Redux store is stored in a React Context that wraps the entire app.
- The
Provider
component from React-Redux is simply a wrapper around this Redux context provider. - This allows any component within the app to access the Redux store without passing props manually.
Example: How Provider
Works
import { Provider } from "react-redux";
import { store } from "./store";
import App from "./App";
const Root = () => (
<Provider store={store}>
<App />
</Provider>
);
Internally, Provider
stores the Redux store inside a React Context and makes it accessible to all components.
How useSelector
Works Under the Hood
The useSelector
hook is responsible for selecting a piece of state from the Redux store inside a component.
How It Works:
- It gets the Redux store from context
-
It subscribes to state updates using
useSyncExternalStore
, a built-in React hook for listening to external stores - It extracts only the relevant part of the state (to optimize performance)
Example: Using useSelector
import { useSelector } from "react-redux";
const Counter = () => {
const count = useSelector(state => state.counter.value);
return <div>Count: {count}</div>;
};
Why useSyncExternalStore
?
- React-Redux used to rely on
useEffect
anduseState
for subscriptions, but this led to performance issues and stale state bugs. -
useSyncExternalStore
ensures that React always reads the latest state and minimizes unnecessary re-renders.
How useDispatch
Works Under the Hood
The useDispatch
hook allows components to dispatch actions to the Redux store.
How It Works:
- It gets the Redux store from context
- It returns the store’s
dispatch
function
Example: Using useDispatch
import { useDispatch } from "react-redux";
import { increment } from "./counterSlice";
const IncrementButton = () => {
const dispatch = useDispatch();
return <button onClick={() => dispatch(increment())}>Increment</button>;
};
Unlike useSelector
, useDispatch
does not subscribe to state changes—it only provides the dispatch
function.
Optimisations in React-Redux
-
Context is only used for accessing the store, not state updates
- Redux’s state changes do not trigger unnecessary re-renders like regular React Context.
-
useSelector
prevents unnecessary re-renders- Components only re-render when the selected state changes, thanks to
useSyncExternalStore
.
- Components only re-render when the selected state changes, thanks to
Wrapping Up: Understanding Redux Inside Out
By now, you should have a clear understanding of how Redux and React-Redux work under the hood. We explored:
✅ Redux Toolkit (RTK) – The core state management logic built on the pub-sub model, where a store holds the state, reducers handle updates immutably, and subscribed listeners react to changes.
✅ React-Redux – The binding layer that efficiently connects Redux to React using React Context and hooks like useSelector
and useDispatch
, leveraging useSyncExternalStore
for optimal reactivity.
Understanding these internals helps you:
🔹 Debug complex state issues with confidence
🔹 Optimize performance by avoiding unnecessary re-renders
🔹 Customize Redux behavior or even build your own minimal Redux-like system
At the end of the day, Redux is just a predictable state container with a well-defined flow. While libraries like Redux Toolkit and React-Redux make it easier to use, the core principles remain simple: dispatch an action → reducer updates the state → components react accordingly.
Next time you're using Redux, take a moment to appreciate the elegant architecture behind it.
Top comments (0)