Redux Pattern — Predictable State Container with Actions and Reducers
In this tutorial, you will learn about Redux Pattern. We cover key concepts, practical examples, and best practices to help you master this topic.
Redux is a predictable state container that uses actions, reducers, and a single store to manage application state with unidirectional data flow and time-travel debugging.
What You'll Learn
By the end of this tutorial, you will understand Redux core concepts (store, actions, reducers), the unidirectional data flow pattern, how to integrate Redux with React, and when Redux is beneficial.
Why It Matters
Redux is the most widely used state management library for SPAs. Understanding its patterns — even if you choose alternatives — teaches you disciplined state management: immutability, predictable updates, and separation of concerns.
Real-World Use
A large e-commerce SPA uses Redux to manage product catalog, shopping cart, user preferences, and order history. The single store with DevTools allows developers to replay state changes to debug complex checkout issues that would be nearly impossible to trace in component-local state.
Redux Architecture
Redux Data Flow
┌──────────────┐
│ Component │ (UI renders state)
│ (React/Vue) │
└──────┬───────┘
│ User clicks button
▼
┌──────────────┐
│ dispatch │ (component calls store.dispatch)
│ (action) │ { type: 'ADD_TODO', payload: 'Buy milk' }
└──────┬───────┘
▼
┌──────────────┐
│ Reducer │ (pure function: (state, action) => newState)
│ (pure fn) │
└──────┬───────┘
▼
┌──────────────┐
│ Store │ (new state computed, stored)
└──────┬───────┘
▼
┌──────────────┐
│ Component │ (re-renders with new state)
└──────────────┘
Think of Redux like a bank. The store is your bank account balance. Actions are bank transactions (deposit, withdraw). Reducers are the bank tellers who Process transactions and update your balance. You (the component) ask the teller to process a Transaction, and the balance updates.
Core Concepts
// 1. Action — describes what happened
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
function addTodo(text) {
return {
type: ADD_TODO,
payload: {
id: Date.now(),
text,
completed: false
}
};
}
function toggleTodo(id) {
return {
type: TOGGLE_TODO,
payload: { id }
};
}
// 2. Reducer — pure function that returns new state
const initialState = {
todos: [],
filter: 'all'
};
function todoReducer(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos, action.payload]
};
case TOGGLE_TODO:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'SET_FILTER':
return {
...state,
filter: action.payload
};
default:
return state;
}
}
// 3. Store — holds state and provides dispatch, subscribe, getState
function createStore(reducer) {
let state = reducer(undefined, { type: '@@INIT' });
let listeners = [];
return {
getState: () => state,
dispatch: (action) => {
state = reducer(state, action);
listeners.forEach(listener => listener());
},
subscribe: (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter(l => l !== listener);
};
}
};
}
// Usage
const store = createStore(todoReducer);
store.subscribe(() => {
console.log('State updated:', store.getState());
});
store.dispatch(addTodo('Learn Redux'));
store.dispatch(addTodo('Build an app'));
store.dispatch(toggleTodo(1));
Output:
State updated: { todos: [{ id: 1, text: 'Learn Redux', completed: false }], filter: 'all' }
State updated: { todos: [{ id: 1, ...}, { id: 2, text: 'Build an app', ...}], filter: 'all' }
State updated: { todos: [{ id: 1, text: 'Learn Redux', completed: true }, ...], filter: 'all' }
Redux with React (using Redux Toolkit)
// store/todosSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Async thunk for API calls
export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
const response = await fetch('/api/todos');
return response.json();
});
const todosSlice = createSlice({
name: 'todos',
initialState: {
items: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
filter: 'all'
},
reducers: {
addTodo: {
reducer(state, action) {
state.items.push(action.payload);
},
prepare(text) {
return {
payload: {
id: Date.now(),
text,
completed: false
}
};
}
},
toggleTodo(state, action) {
const todo = state.items.find(t => t.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
setFilter(state, action) {
state.filter = action.payload;
}
},
extraReducers(builder) {
builder
.addCase(fetchTodos.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
}
});
export const { addTodo, toggleTodo, setFilter } = todosSlice.actions;
export default todosSlice.reducer;
// store/store.js
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from './todosSlice';
export const store = configureStore({
reducer: {
todos: todosReducer
}
});
Using Redux in React Components
import { useSelector, useDispatch } from 'react-redux';
import { addTodo, toggleTodo, fetchTodos } from './store/todosSlice';
function TodoList() {
const dispatch = useDispatch();
const { items, status, error, filter } = useSelector(state => state.todos);
useEffect(() => {
dispatch(fetchTodos());
}, [dispatch]);
const filteredTodos = items.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
if (status === 'loading') return <div>Loading...</div>;
if (status === 'failed') return <div>Error: {error}</div>;
return (
<div>
<input
placeholder="Add todo"
onKeyDown={(e) => {
if (e.key === 'Enter' && e.target.value) {
dispatch(addTodo(e.target.value));
e.target.value = '';
}
}}
/>
<ul>
{filteredTodos.map(todo => (
<li
key={todo.id}
onClick={() => dispatch(toggleTodo(todo.id))}
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
cursor: 'pointer'
}}
>
{todo.text}
</li>
))}
</ul>
</div>
);
}
Redux Middleware
// Logger middleware (runs between dispatch and reducer)
const loggerMiddleware = (store) => (next) => (action) => {
console.log('Dispatching:', action.type, action.payload);
console.log('Previous state:', store.getState());
const result = next(action);
console.log('Next state:', store.getState());
return result;
};
// Configure store with middleware
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(loggerMiddleware)
});
Common Mistakes
- Mutating state in reducers. Redux reducers must be pure functions. Never mutate state directly. Use spread operators or Immer (built into Redux Toolkit).
- Putting non-serializable data in state. Functions, Promises, and class instances should not be in Redux state. Keep state serializable for devtools.
- Too many separate Redux stores. Use a single store with multiple slices. Multiple stores lose the benefits of Redux.
- Over-normalizing state. Not everything needs to be in Redux. Local component state is fine for form inputs and UI toggles.
- Not using Redux Toolkit. Writing Redux manually with action types, action creators, and reducers is verbose. Redux Toolkit simplifies all of this.
Practice Questions
- What are the three core principles of Redux?
- What is a reducer and why must it be a pure function?
- How does unidirectional data flow work in Redux?
- What problem does Redux Toolkit solve?
- What is middleware in Redux and when would you use it?
Challenge: Build a complete Redux-powered todo application using Redux Toolkit: add, toggle, delete, and filter todos. Include an async thunk that fetches initial todos from a mock API. Use useSelector and useDispatch in components.
FAQ
Mini Project
Build a Redux-powered shopping cart: store with products slice and cart slice, async thunk to fetch products, add/remove items from cart, calculate totals, and persist cart state to localStorage. Use Redux Toolkit and React-Redux hooks.
What's Next
You mastered Redux. Now explore the Context API — React's built-in state sharing mechanism that complements or replaces Redux for simpler use cases.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro