Redux is more than a state management library. It is a predictable architecture for managing application state through explicit actions, pure state transitions, and a centralized store.
If you have worked with React, Angular, or modern frontend applications, you have probably encountered problems such as:
- Multiple components needing the same state
- State being passed through many layers of components
- Difficult-to-debug state changes
- Complex asynchronous operations
- Inconsistent application state
- Business logic scattered across components
Redux was designed to solve these problems by introducing a predictable and structured approach to state management.
In this article, we will go from the fundamentals to practical Redux development, including:
- What Redux is
- Why Redux exists
- Core Redux concepts
- Store
- Actions
- Reducers
- Dispatch
- Selectors
- Immutability
- Redux data flow
- Middleware
- Async operations
- Redux Toolkit
- RTK Query
- Entity management
- Real-world architecture
- Common mistakes
- A complete example
1. What Is Redux?
Redux is a predictable state management library.
The basic idea is simple:
UI
↓
Dispatch Action
↓
Reducer
↓
New State
↓
Store
↓
UI Updates
Instead of allowing components to modify application state however they want, Redux creates a controlled flow.
For example:
dispatch({
type: "counter/increment"
});
The action reaches a reducer:
function counterReducer(state, action) {
if (action.type === "counter/increment") {
return {
...state,
value: state.value + 1
};
}
return state;
}
The reducer produces the next state.
2. Why Do We Need Redux?
Consider a large application.
You may have:
App
├── Header
│ └── UserMenu
│ └── UserProfile
│
├── Dashboard
│ ├── Statistics
│ ├── Orders
│ └── Notifications
│
└── Sidebar
Suppose the logged-in user is needed by:
- Header
- UserMenu
- Dashboard
- Sidebar
- Notifications
Without centralized state management, you may end up passing:
App
↓
Header
↓
UserMenu
↓
UserProfile
This is commonly called prop drilling.
Redux provides a centralized store:
Redux Store
/ | \
↓ ↓ ↓
Header Dashboard Sidebar
Components can subscribe to the state they need.
3. Redux Core Principles
Redux is based on several important principles.
3.1 Single Source of Truth
Application state is stored in one centralized store.
{
user: {
id: 1,
name: "Abanoub"
},
cart: {
items: []
},
products: [],
ui: {
theme: "dark"
}
}
Instead of having unrelated copies of important state throughout the application, Redux provides a central source.
4. State Is Read-Only
Components should not directly modify Redux state.
Incorrect:
state.counter.value++;
Instead, dispatch an action:
dispatch({
type: "counter/increment"
});
The reducer determines how the state changes.
5. Changes Are Made Through Pure Functions
Reducers are responsible for calculating the next state.
Conceptually:
Previous State + Action = Next State
Example:
const previousState = {
value: 10
};
const action = {
type: "increment"
};
const nextState = {
value: 11
};
The reducer:
function reducer(state, action) {
switch (action.type) {
case "increment":
return {
...state,
value: state.value + 1
};
default:
return state;
}
}
6. The Redux Store
The store contains the application state.
With modern Redux, the recommended approach is Redux Toolkit.
import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({
reducer: {
counter: counterReducer
}
});
Conceptually:
Store
│
├── counter
├── user
├── products
├── cart
└── notifications
7. Actions
An action describes what happened.
Example:
{
type: "counter/increment"
}
Another example:
{
type: "cart/addItem",
payload: {
id: 10,
name: "Keyboard"
}
}
The action does not directly modify state.
It describes an event.
8. Action Types
An action type is usually a string.
{
type: "user/login"
}
Examples:
user/login
user/logout
cart/addItem
cart/removeItem
products/load
products/delete
A useful naming convention is:
feature/event
For example:
cart/addItem
9. Payload
The payload contains additional information.
{
type: "cart/addItem",
payload: {
id: 1,
name: "Laptop",
price: 1200
}
}
Another example:
{
type: "user/setUser",
payload: {
id: 5,
name: "John"
}
}
10. Reducers
A reducer receives:
Current State
+
Action
and returns:
Next State
Example:
function counterReducer(state = { value: 0 }, action) {
switch (action.type) {
case "increment":
return {
...state,
value: state.value + 1
};
case "decrement":
return {
...state,
value: state.value - 1
};
default:
return state;
}
}
A reducer should be:
- Predictable
- Pure
- Deterministic
- Free of side effects
11. What Does "Pure Function" Mean?
A pure function:
- Produces the same output for the same input.
- Does not modify external state.
- Does not perform side effects.
Example:
function add(a, b) {
return a + b;
}
This is pure.
But:
let total = 0;
function add(value) {
total += value;
}
This is not pure because it modifies external state.
Reducers should follow the pure-function principle.
12. Dispatch
Dispatch sends an action to Redux.
dispatch({
type: "counter/increment"
});
The flow becomes:
Component
↓
dispatch(action)
↓
Redux
↓
Reducer
↓
New State
↓
Store
↓
Subscribed Components
13. Selectors
Selectors read data from the Redux store.
For example:
const selectCount = state => state.counter.value;
Then:
const count = useSelector(selectCount);
Selectors help keep components independent from the exact shape of the state.
Instead of:
state.counter.value
everywhere, you can use:
selectCount(state)
14. Redux Data Flow
Redux follows a predictable one-way data flow.
┌─────────────┐
│ UI │
└──────┬──────┘
│
│ dispatch()
↓
┌─────────────┐
│ Action │
└──────┬──────┘
↓
┌─────────────┐
│ Reducer │
└──────┬──────┘
↓
┌─────────────┐
│ Store │
└──────┬──────┘
↓
┌─────────────┐
│ UI │
└─────────────┘
This predictable flow is one of Redux's biggest advantages.
15. A Simple Redux Example
Let's create a counter.
With Redux Toolkit:
import { createSlice, configureStore } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: {
value: 0
},
reducers: {
increment(state) {
state.value += 1;
},
decrement(state) {
state.value -= 1;
},
incrementByAmount(state, action) {
state.value += action.payload;
}
}
});
Export the actions:
export const {
increment,
decrement,
incrementByAmount
} = counterSlice.actions;
Create the store:
const store = configureStore({
reducer: {
counter: counterSlice.reducer
}
});
Now:
store.dispatch(increment());
Or:
store.dispatch(incrementByAmount(10));
16. Why Does Redux Toolkit Allow Mutation?
You might notice:
state.value += 1;
Earlier we said Redux state should not be mutated.
So why does this work?
Redux Toolkit uses Immer internally.
Immer allows you to write:
state.value += 1;
while internally producing an immutable state update.
Conceptually:
Your Code
↓
Immer
↓
Immutable Update
↓
Redux State
This gives developers simpler syntax while preserving Redux's immutability model.
17. createSlice
createSlice() is one of the most important Redux Toolkit APIs.
It combines:
- State
- Reducers
- Action creators
- Action types
Instead of manually writing:
const INCREMENT = "counter/increment";
function increment() {
return {
type: INCREMENT
};
}
function reducer(state, action) {
...
}
you can write:
const counterSlice = createSlice({
name: "counter",
initialState: {
value: 0
},
reducers: {
increment(state) {
state.value++;
}
}
});
Redux Toolkit generates the action creator automatically.
18. Payload Actions
Suppose we want to add a product.
const cartSlice = createSlice({
name: "cart",
initialState: {
items: []
},
reducers: {
addItem(state, action) {
state.items.push(action.payload);
}
}
});
Dispatch:
dispatch(
addItem({
id: 1,
name: "Laptop",
price: 1200
})
);
The action becomes conceptually:
{
type: "cart/addItem",
payload: {
id: 1,
name: "Laptop",
price: 1200
}
}
19. Redux With React
Redux itself is independent of React.
To integrate Redux with React, we commonly use React-Redux.
First create the store:
const store = configureStore({
reducer: {
counter: counterReducer
}
});
Then provide it to React:
import { Provider } from "react-redux";
<Provider store={store}>
<App />
</Provider>
Now components can access Redux.
20. useSelector
useSelector() reads data.
import { useSelector } from "react-redux";
function Counter() {
const count = useSelector(
state => state.counter.value
);
return <h1>{count}</h1>;
}
When the selected state changes, the component can re-render.
21. useDispatch
useDispatch() allows a component to dispatch actions.
import { useDispatch } from "react-redux";
import { increment } from "./counterSlice";
function CounterButton() {
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(increment())}>
Increment
</button>
);
}
22. Complete React + Redux Example
function Counter() {
const count = useSelector(
state => state.counter.value
);
const dispatch = useDispatch();
return (
<div>
<h1>{count}</h1>
<button
onClick={() => dispatch(increment())}
>
+
</button>
<button
onClick={() => dispatch(decrement())}
>
-
</button>
</div>
);
}
The component does not directly change:
state.counter.value
Instead:
Button
↓
dispatch(increment())
↓
Reducer
↓
New State
↓
useSelector
↓
Component Re-render
23. Local State vs Redux State
Not every piece of state belongs in Redux.
For example:
const [isOpen, setIsOpen] = useState(false);
This is usually local UI state.
Redux is more appropriate for state that needs to be shared or coordinated across different parts of an application.
Local State
Examples:
Modal open/closed
Input value
Dropdown state
Temporary UI state
Global State
Examples:
Authenticated user
Shopping cart
Permissions
Global notifications
Shared application configuration
Cached server data
24. Redux Is Not Always Necessary
A common mistake is:
"Every React application should use Redux."
Not true.
For a small application:
React
+
useState
+
useContext
may be enough.
Redux becomes more valuable as state complexity increases.
A useful question is:
Is the complexity of shared state becoming harder to manage than the complexity of introducing Redux?
25. Middleware
Middleware sits between:
dispatch()
↓
Middleware
↓
Reducer
It can:
- Log actions
- Perform asynchronous operations
- Dispatch additional actions
- Handle side effects
- Integrate external services
Conceptually:
dispatch(action)
↓
Middleware
↓
Reducer
↓
Store
26. Why Do We Need Middleware?
Reducers should be pure.
Therefore, things like:
fetch()
should not normally happen inside reducers.
Incorrect:
function reducer(state, action) {
fetch("/api/products");
return state;
}
Instead, asynchronous work should happen outside reducers, commonly through middleware.
27. Redux Thunk
Redux Toolkit includes thunk middleware by default.
A thunk allows you to dispatch a function-like async workflow.
Example:
const fetchUsers = () => async dispatch => {
dispatch(usersLoading());
try {
const response = await fetch("/api/users");
const users = await response.json();
dispatch(usersLoaded(users));
} catch (error) {
dispatch(usersFailed(error.message));
}
};
Then:
dispatch(fetchUsers());
The flow becomes:
Component
↓
dispatch(fetchUsers())
↓
Thunk
↓
API Request
↓
dispatch(usersLoaded())
↓
Reducer
↓
Store
28. createAsyncThunk
Redux Toolkit provides createAsyncThunk() to simplify common async workflows.
export const fetchUsers = createAsyncThunk(
"users/fetchUsers",
async () => {
const response = await fetch("/api/users");
return response.json();
}
);
Then handle the lifecycle:
const usersSlice = createSlice({
name: "users",
initialState: {
data: [],
loading: false,
error: null
},
extraReducers: builder => {
builder
.addCase(fetchUsers.pending, state => {
state.loading = true;
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
}
});
Now Redux automatically provides:
pending
fulfilled
rejected
29. Handling API State
A common pattern is:
{
data: [],
loading: false,
error: null
}
The state transitions are:
Initial
↓
loading = true
↓
API Request
↓
┌───────────────┐
│ │
Success Failure
│ │
↓ ↓
data error
This makes asynchronous state explicit.
30. RTK Query
For server data, Redux Toolkit provides RTK Query.
RTK Query is designed specifically for:
- Fetching data
- Caching
- Refetching
- Loading states
- Error states
- Request deduplication
- Cache invalidation
Example:
const api = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({
baseUrl: "/api"
}),
endpoints: builder => ({
getUsers: builder.query({
query: () => "/users"
})
})
});
Then:
const {
data,
isLoading,
error
} = useGetUsersQuery();
This removes a lot of manual async-state management.
31. Redux Toolkit vs Traditional Redux
Traditional Redux often required:
Action Types
Action Creators
Reducers
Store
Middleware
Selectors
with a lot of boilerplate.
Modern Redux recommends Redux Toolkit.
Instead of:
const INCREMENT = "INCREMENT";
const increment = () => ({
type: INCREMENT
});
function reducer(state, action) {
switch (action.type) {
...
}
}
you can use:
const counterSlice = createSlice({
name: "counter",
initialState: {
value: 0
},
reducers: {
increment(state) {
state.value++;
}
}
});
Redux Toolkit is now the recommended way to write Redux applications.
32. Normalizing State
Suppose you have:
{
users: [
{
id: 1,
name: "John"
},
{
id: 2,
name: "Sarah"
}
]
}
As applications grow, searching and updating entities can become inefficient.
Normalized state can look like:
{
users: {
ids: [1, 2],
entities: {
1: {
id: 1,
name: "John"
},
2: {
id: 2,
name: "Sarah"
}
}
}
}
Redux Toolkit provides:
createEntityAdapter()
for this use case.
33. createEntityAdapter
Example:
const usersAdapter = createEntityAdapter();
const initialState =
usersAdapter.getInitialState();
You can then use generated reducers and selectors to manage entities efficiently.
This is especially useful for:
Users
Products
Orders
Messages
Notifications
34. Selectors in Large Applications
Instead of exposing state structure everywhere:
state.products.entities
create selectors:
const selectProducts =
state => state.products.entities;
Then components use:
const products = useSelector(selectProducts);
This improves maintainability.
35. Derived State
Sometimes you don't need to store everything.
For example, suppose Redux contains:
{
products: [
{ price: 100 },
{ price: 200 },
{ price: 300 }
]
}
You don't necessarily need:
{
products: [...],
totalPrice: 600
}
You can derive it:
const selectTotalPrice = state =>
state.products.reduce(
(total, product) => total + product.price,
0
);
This avoids duplicated state.
36. Memoized Selectors
For expensive calculations, selectors can be memoized.
Redux Toolkit works well with Reselect-style selectors.
Example:
const selectCompletedTodos = createSelector(
[selectTodos],
todos =>
todos.filter(todo => todo.completed)
);
The selector can avoid recalculating when its inputs haven't changed.
37. Redux Architecture
A scalable Redux application can be organized by features:
src/
│
├── app/
│ └── store.js
│
├── features/
│ ├── auth/
│ │ ├── authSlice.js
│ │ ├── authSelectors.js
│ │ └── authApi.js
│ │
│ ├── products/
│ │ ├── productsSlice.js
│ │ ├── productsSelectors.js
│ │ └── productsApi.js
│ │
│ └── cart/
│ ├── cartSlice.js
│ └── cartSelectors.js
│
└── components/
This is called feature-based organization.
It scales much better than organizing everything by technical type.
38. Real-World Example: E-Commerce
Imagine an e-commerce application.
We might have:
Redux Store
│
├── auth
│
├── cart
│
├── products
│
├── orders
│
└── ui
Example:
{
auth: {
user: null,
token: null
},
cart: {
items: []
},
products: {
ids: [],
entities: {}
},
orders: {
ids: [],
entities: {}
},
ui: {
sidebarOpen: false
}
}
39. Adding a Product to the Cart
User clicks:
Add to Cart
The component dispatches:
dispatch(
addToCart({
productId: 10,
quantity: 1
})
);
Redux flow:
Product Component
↓
addToCart()
↓
Action
↓
Cart Reducer
↓
Cart State Updated
↓
Cart Icon
↓
UI Updated
The cart icon can now select:
const itemCount = useSelector(
selectCartItemCount
);
40. Authentication Example
Suppose a user logs in.
The UI dispatches:
dispatch(
loginSuccess({
id: 1,
name: "John"
})
);
The reducer updates:
{
user: {
id: 1,
name: "John"
},
isAuthenticated: true
}
Other components can react to this state.
For example:
const user = useSelector(selectCurrentUser);
41. Redux DevTools
One of Redux's biggest advantages is debugging.
Redux DevTools can show:
Action
↓
Previous State
↓
Action Payload
↓
Next State
For example:
cart/addItem
You can inspect:
{
productId: 10,
quantity: 2
}
and then compare the previous and next state.
This makes complex state transitions easier to debug.
42. Time-Travel Debugging
Because Redux state transitions are explicit:
State 0
↓
Action A
↓
State 1
↓
Action B
↓
State 2
↓
Action C
↓
State 3
development tools can replay state transitions.
This is one of the conceptual reasons Redux became popular.
43. Common Redux Mistakes
Mistake 1: Putting Everything in Redux
Don't put every UI detail into Redux.
Avoid:
{
modalIsOpen: true,
inputValue: "hello",
hoverState: true
}
unless there is a genuine reason for global access.
Mistake 2: Mutating State Outside Immer
Do not do:
const user = store.getState().user;
user.name = "New Name";
State should be changed through Redux actions and reducers.
Mistake 3: Putting API Calls in Reducers
Avoid:
function reducer(state, action) {
fetch("/api/users");
return state;
}
Reducers should remain pure.
Mistake 4: Duplicating Derived Data
Avoid storing:
{
items: [...],
itemCount: 5
}
if itemCount can simply be calculated from items.
Mistake 5: Huge Global Slice
Avoid creating:
appSlice.js
with hundreds of unrelated responsibilities.
Prefer:
authSlice
cartSlice
productsSlice
ordersSlice
notificationsSlice
44. Redux vs Context API
React Context and Redux solve related but different problems.
Context
Good for:
Theme
Locale
Authentication context
Simple global configuration
Redux
Useful when you have:
Complex state
Many state transitions
Multiple consumers
Complex async workflows
Need for powerful debugging
Normalized entities
Large application state
Context is not automatically a replacement for Redux.
45. Redux vs useState
useState is excellent for local state.
const [count, setCount] = useState(0);
Redux becomes useful when state needs to be shared and the update logic becomes complex.
Think:
Simple local state
↓
useState
versus:
Complex shared application state
↓
Redux
46. Redux vs Zustand
Redux and Zustand are both state management solutions.
Redux provides a more structured architecture:
Actions
↓
Reducers
↓
Store
Zustand generally provides a simpler API with less ceremony.
Redux is often preferred when:
- The application is large
- A standardized architecture matters
- Teams need predictable conventions
- Advanced Redux tooling is valuable
- Existing ecosystem integrations are important
Zustand can be attractive when simplicity and minimal boilerplate are priorities.
47. A Practical Redux Mental Model
If you remember only one model, remember this:
STATE
↑
REDUCER
↑
ACTION
↑
DISPATCH
↑
UI
Or:
User Interaction
↓
Action
↓
Reducer
↓
New State
↓
UI
48. Complete Mini Project
Let's combine the concepts.
Store
import { configureStore } from "@reduxjs/toolkit";
import cartReducer from "./cartSlice";
export const store = configureStore({
reducer: {
cart: cartReducer
}
});
Slice
import { createSlice } from "@reduxjs/toolkit";
const cartSlice = createSlice({
name: "cart",
initialState: {
items: []
},
reducers: {
addItem(state, action) {
const existingItem = state.items.find(
item => item.id === action.payload.id
);
if (existingItem) {
existingItem.quantity += 1;
} else {
state.items.push({
...action.payload,
quantity: 1
});
}
},
removeItem(state, action) {
state.items = state.items.filter(
item => item.id !== action.payload
);
},
clearCart(state) {
state.items = [];
}
}
});
export const {
addItem,
removeItem,
clearCart
} = cartSlice.actions;
export default cartSlice.reducer;
Selector
export const selectCartItems =
state => state.cart.items;
export const selectCartCount =
state =>
state.cart.items.reduce(
(total, item) => total + item.quantity,
0
);
Component
function Cart() {
const items = useSelector(selectCartItems);
const count = useSelector(selectCartCount);
const dispatch = useDispatch();
return (
<div>
<h1>Cart ({count})</h1>
{items.map(item => (
<div key={item.id}>
{item.name}
<button
onClick={() =>
dispatch(removeItem(item.id))
}
>
Remove
</button>
</div>
))}
<button
onClick={() => dispatch(clearCart())}
>
Clear Cart
</button>
</div>
);
}
This is already a realistic Redux pattern.
49. The Most Important Redux APIs
Modern Redux development commonly revolves around:
configureStore()
createSlice()
createAsyncThunk()
createEntityAdapter()
createSelector()
createApi()
fetchBaseQuery()
React integration commonly uses:
Provider
useSelector()
useDispatch()
You do not need to memorize everything immediately.
Understand the architecture first.
50. Redux in One Diagram
┌──────────────┐
│ UI │
└──────┬───────┘
│
dispatch(action)
│
↓
┌──────────────┐
│ Middleware │
└──────┬───────┘
│
↓
┌──────────────┐
│ Reducer │
└──────┬───────┘
│
New Immutable State
│
↓
┌──────────────┐
│ Store │
└──────┬───────┘
│
subscribe/select
│
↓
┌──────────────┐
│ UI │
└──────────────┘
Conclusion
Redux is fundamentally about predictable state transitions.
The most important concepts are:
Store
Actions
Reducers
Dispatch
Selectors
Middleware
Immutability
Modern Redux development should generally use Redux Toolkit rather than manually writing the older Redux boilerplate.
The key mental model is:
UI
↓
dispatch(Action)
↓
Middleware
↓
Reducer
↓
New State
↓
Store
↓
Selectors
↓
UI
Once you understand this flow, advanced Redux concepts such as asynchronous actions, RTK Query, entity adapters, memoized selectors, and large-scale Redux architecture become much easier to understand.
Redux isn't simply a place to put variables.
It is an architecture for making state changes explicit, predictable, traceable, and easier to manage as an application grows.

Top comments (0)