Understanding Redux
Redux is a JavaScript framework that manages the frontend state of web applications. Think of Redux as a central bank for your application's data - it stores information in an organized manner and allows quick retrieval from anywhere in the app.
Imagine building a multi-story shopping mall. Each store needs access to customer information, inventory details, and payment systems. Without a central management system, each store would need to maintain its own copies of this data, leading to inconsistencies and confusion. Redux solves this problem by creating a single "source of truth" - a central database that all parts of your application can access.
Key Advantages of Redux
- Simplicity: It simplifies the more cumbersome aspects of state management
- Lightweight: The library is only 2 KB in size
- Performance: Very fast data insertion and retrieval
- Predictability: The same action always produces the same result
The Origins of Redux
Redux was created by Dan Abramov in 2015 as an experiment to simplify Flux, another state management pattern. Abramov wanted to remove unnecessary boilerplate code and address frustrations in the debugging process.
One particular pain point was having to repeatedly go through a series of steps to trigger a bug during development. Abramov envisioned developer tools that would allow undoing or replaying actions with a simple click - this vision became the Redux DevTools.
Redux's design is built on three core principles:
- Single Source of Truth: The entire application state is stored in one JavaScript object
- State is Read-Only: State cannot be directly modified, only through dispatched actions
- Changes Use Pure Functions: Reducers that process actions and return updated state are pure functions
When Should You Use Redux?
Redux has grown significantly in popularity since its creation, with millions of downloads and adoption by companies like Exana, Patreon, and ClassPass. However, it's not always the best choice for every project.
Redux vs. React Context
Since Redux's introduction, React has added Context, which also provides global state management.
Use Context when:
- Your application has simpler global state requirements
- You want to avoid adding extra dependencies
- You prefer a simpler setup with less boilerplate
Use Redux when:
- Your application has complex state logic
- You need more sophisticated middleware support
- You want advanced debugging with time-travel capabilities
- Your state changes are numerous and intricate
- Many components need access to the same state
A good rule of thumb: start with local state, move to Context when state needs to be shared, and adopt Redux when state management becomes more complex.
Core Redux Concepts
State
State represents all the information stored by your application at a particular point in time. Unlike the logic of your program which remains constant, state changes as users interact with your application.
Think of state as a snapshot of your application at a specific moment - like a photograph that captures the exact position of everything in a room at the instant it was taken.
Store
The Redux store is a single JavaScript object that holds the entire state of your application. It provides a few essential methods:
getState(): Retrieves the current state
dispatch(action): Sends an action to update the state
subscribe(listener): Registers a function to be called when state changes
Imagine the store as a secure vault that holds your application's valuable data. You can look at what's inside (getState), you can make requests to change its contents (dispatch), and you can ask to be notified when something changes (subscribe).
Actions
Actions are plain JavaScript objects that describe what happened in your application. They must have a type property and can include additional data.
Think of actions as "event tickets" in your application. Each ticket describes an event that occurred (button click, form submission, API response) and might contain additional details about the event.
// A simple action
{
type: 'ADD_TODO',
text: 'Learn Redux'
}
// An action creator function
function addTodo(text) {
return {
type: 'ADD_TODO',
text
};
}
Reducers
Reducers are pure functions that take the current state and an action as arguments, then return a new state. They determine how the state should change in response to actions.
Think of reducers as processors in an assembly line. Each processor receives an item (the current state), reads an instruction card (the action), follows the instruction to modify the item, and then passes the modified item down the line.
// A simple reducer
function todoReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{ text: action.text, completed: false }
];
case 'TOGGLE_TODO':
return state.map((todo, index) => {
if (index === action.index) {
return { ...todo, completed: !todo.completed };
}
return todo;
});
default:
return state;
}
}
The key characteristic of reducers is that they are pure functions - given the same inputs, they always produce the same output, and they don't modify their inputs or cause side effects.
Middleware
Middleware provides a way to interact with actions before they reach the reducer. It's commonly used for logging, crash reporting, handling asynchronous operations, and more.
Imagine middleware as security checkpoints between the dispatch of an action and when it reaches the reducer. Each checkpoint can inspect the action, modify it, stop it, or trigger additional actions.
Practical Example: Building a Todo App with Redux
Let's build a simple todo application to demonstrate Redux in action.
Project Setup
First, create a new project folder structure:
redux_todo_app/
├── index.html
├── src/
│ ├── index.js
│ ├── actions/
│ │ └── index.js
│ ├── reducers/
│ │ ├── index.js
│ │ └── todos.js
│ └── components/
│ ├── App.js
│ ├── TodoList.js
│ └── TodoItem.js
Install the required dependencies using npm:
npm install redux react-redux
1. Define Actions
Let's create our action types and action creators in src/actions/index.js:
// Action Types
export const ADD_TODO = 'ADD_TODO';
export const TOGGLE_TODO = 'TOGGLE_TODO';
export const DELETE_TODO = 'DELETE_TODO';
// Action Creators
export const addTodo = (text) => ({
type: ADD_TODO,
payload: {
id: Date.now(),
text,
completed: false
}
});
export const toggleTodo = (id) => ({
type: TOGGLE_TODO,
payload: { id }
});
export const deleteTodo = (id) => ({
type: DELETE_TODO,
payload: { id }
});
These actions define the operations that can be performed on our todos. Think of them as the menu of available operations in a restaurant - they tell the system what changes are possible.
2. Create Reducers
Now let's implement our reducer in src/reducers/todos.js:
import { ADD_TODO, TOGGLE_TODO, DELETE_TODO } from '../actions';
const initialState = [];
const todosReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TODO:
return [...state, action.payload];
case TOGGLE_TODO:
return state.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
);
case DELETE_TODO:
return state.filter(todo => todo.id !== action.payload.id);
default:
return state;
}
};
export default todosReducer;
Our reducer handles each action type with a specific state transformation. Like a chef who knows exactly how to prepare each dish on the menu.
Next, let's combine our reducers in src/reducers/index.js (this becomes more important as your app grows and you add more reducers):
import { combineReducers } from 'redux';
import todosReducer from './todos';
const rootReducer = combineReducers({
todos: todosReducer
});
export default rootReducer;
3. Create the Store
Now let's create our Redux store in src/index.js:
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
export default store;
The second argument enables the Redux DevTools, allowing you to inspect state changes in your browser.
4. Connect Components
Now let's create our components and connect them to the Redux store. First, src/components/TodoItem.js:
import React from 'react';
import { useDispatch } from 'react-redux';
import { toggleTodo, deleteTodo } from '../actions';
const TodoItem = ({ todo }) => {
const dispatch = useDispatch();
return (
<li className="todo-item">
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch(toggleTodo(todo.id))}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<button
onClick={() => dispatch(deleteTodo(todo.id))}
>
Delete
</button>
</li>
);
};
export default TodoItem;
Next, src/components/TodoList.js:
import React from 'react';
import { useSelector } from 'react-redux';
import TodoItem from './TodoItem';
const TodoList = () => {
const todos = useSelector(state => state.todos);
return (
<ul className="todo-list">
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
};
export default TodoList;
Finally, src/components/App.js:
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addTodo } from '../actions';
import TodoList from './TodoList';
const App = () => {
const [text, setText] = useState('');
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
dispatch(addTodo(text));
setText('');
};
return (
<div className="app">
<h1>Redux Todo App</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add a new todo..."
/>
<button type="submit">Add Todo</button>
</form>
<TodoList />
</div>
);
};
export default App;
5. Connect the Store to React
Finally, let's update our main src/index.js file to connect the Redux store to our React application:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './components/App';
import './index.css';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
The Provider component makes the Redux store available to any nested components that need to access it.
Advanced Redux Topics
Asynchronous Actions with Redux Thunk
Redux itself is synchronous, but real-world applications often need to perform asynchronous operations like API calls. Redux Thunk is middleware that allows action creators to return functions instead of plain objects.
A thunk function receives the store's dispatch and getState methods as arguments, allowing it to dispatch multiple actions and access the current state.
// First, install the middleware:
// npm install redux-thunk
// Then apply it to your store:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
// Now you can write thunk action creators:
export const fetchTodos = () => {
return async (dispatch) => {
dispatch({ type: 'FETCH_TODOS_REQUEST' });
try {
const response = await fetch('https://api.example.com/todos');
const data = await response.json();
dispatch({
type: 'FETCH_TODOS_SUCCESS',
payload: data
});
} catch (error) {
dispatch({
type: 'FETCH_TODOS_FAILURE',
error: error.message
});
}
};
};
This pattern is like ordering food through a delivery app - you place the order (initial dispatch), wait for the restaurant to prepare it (async operation), and then receive a notification when it's ready (success dispatch) or if there's a problem (failure dispatch).
Redux DevTools
Redux DevTools provide powerful debugging capabilities:
- Time-travel debugging: move back and forth through state changes
- Action inspection: see exactly what actions were dispatched
- State inspection: examine the state at any point in time
- Action replay: replay actions one by one
To use Redux DevTools, install the browser extension and connect it to your store:
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
The DevTools are like having a DVR for your application state - you can pause, rewind, and replay every change that occurs.
Selector Functions
Selectors are functions that extract specific pieces of information from the store state. They help keep your components clean and focused on rendering, while the logic for deriving data lives in the selectors.
// Define selectors in a separate file (e.g., selectors.js)
export const getTodos = state => state.todos;
export const getCompletedTodos = state =>
state.todos.filter(todo => todo.completed);
export const getIncompleteTodos = state =>
state.todos.filter(todo => !todo.completed);
// Use them in your components
import { useSelector } from 'react-redux';
import { getCompletedTodos } from '../selectors';
const CompletedTodosList = () => {
const completedTodos = useSelector(getCompletedTodos);
return (
<div>
<h2>Completed Todos</h2>
<ul>
{completedTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
};
Selectors are like personalized filters for your data - they let each component see exactly the information it needs in exactly the format it needs it.
Real-World Redux Applications
E-commerce Platform
In an e-commerce application, Redux can manage:
- Shopping cart contents
- User authentication state
- Product filtering and sorting preferences
- Order history and tracking information
Redux's single store makes it easy to implement features like "save for later" or "recently viewed products" that persist across different pages.
Social Media Dashboard
For a social media application, Redux can handle:
- User feed data
- Notification counters
- Chat/message state
- User preferences and settings
Redux Thunk or Redux Saga would help manage the complex async operations needed for real-time updates and infinite scrolling feeds.
Content Management System
In a CMS, Redux can manage:
- Document editing state
- Undo/redo functionality
- Published/draft content status
- Media library contents
The time-travel debugging capabilities of Redux are particularly valuable for implementing complex editing features with undo/redo functionality.
Redux Best Practices
Structure Your Store Carefully
Keep your Redux store normalized - avoid nesting data and use IDs to reference related items. This approach is similar to a relational database and makes updating specific pieces of state much easier.
Example of a well-structured store:
{
users: {
byId: {
'user1': { id: 'user1', name: 'Jane', role: 'admin' },
'user2': { id: 'user2', name: 'John', role: 'editor' }
},
allIds: ['user1', 'user2']
},
posts: {
byId: {
'post1': { id: 'post1', title: 'Redux Basics', authorId: 'user1' },
'post2': { id: 'post2', title: 'Advanced Redux', authorId: 'user1' }
},
allIds: ['post1', 'post2']
}
}
Use Action Creators
Always use action creators instead of dispatching action objects directly. This ensures consistency and makes it easier to track where actions are being dispatched from.
Keep Reducers Pure
Reducers must be pure functions - they should:
- Not mutate their arguments
- Not perform side effects (API calls, routing transitions, etc.)
- Not call non-pure functions (Date.now(), Math.random(), etc.)
This ensures predictable state updates and enables time-travel debugging.
Use Middleware for Side Effects
Side effects like API calls should be handled with middleware such as Redux Thunk, Redux Saga, or Redux Observable.
Use the Redux DevTools
The Redux DevTools are invaluable for debugging and understanding state changes in your application.
Consider Redux Toolkit
Redux Toolkit is the official, opinionated toolset for efficient Redux development. It includes utilities to simplify common Redux use cases, including store setup, creating reducers, immutable update logic, and even creating entire "slices" of state.
// npm install @reduxjs/toolkit
import { createSlice, configureStore } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: (state, action) => {
// Redux Toolkit allows us to write "mutating" logic in reducers
// It doesn't actually mutate the state because it uses Immer
state.push({
id: Date.now(),
text: action.payload,
completed: false
});
},
toggleTodo: (state, action) => {
const todo = state.find(todo => todo.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
}
}
});
export const { addTodo, toggleTodo } = todosSlice.actions;
const store = configureStore({
reducer: {
todos: todosSlice.reducer
}
});
Exercise: Build a Redux Counter
Let's apply what we've learned by building a simple counter application with Redux.
Exercise Setup
Create a new project folder:
redux_counter_exercise/
├── index.html
└── src/
├── index.js
├── actions.js
├── reducers.js
└── store.js
1. Define the Actions
In src/actions.js, define the action types and action creators:
// Action Types
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const RESET = 'RESET';
export const SET_VALUE = 'SET_VALUE';
// Action Creators
export const increment = (amount = 1) => ({
type: INCREMENT,
payload: amount
});
export const decrement = (amount = 1) => ({
type: DECREMENT,
payload: amount
});
export const reset = () => ({
type: RESET
});
export const setValue = (value) => ({
type: SET_VALUE,
payload: value
});
2. Create the Reducer
In src/reducers.js, implement the counter reducer:
import { INCREMENT, DECREMENT, RESET, SET_VALUE } from './actions';
const initialState = {
count: 0,
history: []
};
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case INCREMENT:
return {
...state,
count: state.count + action.payload,
history: [...state.history, `Incremented by ${action.payload}`]
};
case DECREMENT:
return {
...state,
count: state.count - action.payload,
history: [...state.history, `Decremented by ${action.payload}`]
};
case RESET:
return {
...state,
count: 0,
history: [...state.history, 'Reset to 0']
};
case SET_VALUE:
return {
...state,
count: action.payload,
history: [...state.history, `Set to ${action.payload}`]
};
default:
return state;
}
};
export default counterReducer;
Notice that we're not just tracking the count, but also maintaining a history of operations. This demonstrates how Redux can easily track changes over time.
3. Create the Store
In src/store.js, create the Redux store:
import { createStore } from 'redux';
import counterReducer from './reducers';
const store = createStore(
counterReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
export default store;
4. Create the HTML Interface
Let's create a simple HTML interface in index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Redux Counter</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin-top: 50px;
}
#counter {
font-size: 72px;
margin: 20px;
}
button {
padding: 10px 20px;
margin: 5px;
font-size: 16px;
cursor: pointer;
}
#history {
margin-top: 30px;
border-top: 1px solid #ccc;
padding-top: 10px;
text-align: left;
max-width: 400px;
margin-left: auto;
margin-right: auto;
}
</style>
</head>
<body>
<h1>Redux Counter</h1>
<div id="counter">0</div>
<div>
<button id="increment">Increment (+1)</button>
<button id="increment5">Increment (+5)</button>
<button id="decrement">Decrement (-1)</button>
<button id="decrement5">Decrement (-5)</button>
<button id="reset">Reset</button>
</div>
<div>
<input type="number" id="setValue" placeholder="Enter value">
<button id="setButton">Set Value</button>
</div>
<div id="history">
<h3>History:</h3>
<ul id="historyList"></ul>
</div>
<script src="https://unpkg.com/redux@4.2.0/dist/redux.min.js"></script>
<script src="src/index.js" type="module"></script>
</body>
</html>
5. Connect the UI to Redux
Finally, let's implement the JavaScript to connect our UI to Redux in src/index.js:
import store from './store.js';
import { increment, decrement, reset, setValue } from './actions.js';
// DOM elements
const counterDisplay = document.getElementById('counter');
const incrementBtn = document.getElementById('increment');
const increment5Btn = document.getElementById('increment5');
const decrementBtn = document.getElementById('decrement');
const decrement5Btn = document.getElementById('decrement5');
const resetBtn = document.getElementById('reset');
const setValueInput = document.getElementById('setValue');
const setButton = document.getElementById('setButton');
const historyList = document.getElementById('historyList');
// Update the UI when the state changes
function render() {
const state = store.getState();
counterDisplay.textContent = state.count;
// Update history list
historyList.innerHTML = '';
state.history.slice(-5).forEach(action => {
const li = document.createElement('li');
li.textContent = action;
historyList.appendChild(li);
});
}
// Subscribe to store updates
store.subscribe(render);
// Initial render
render();
// Wire up the buttons
incrementBtn.addEventListener('click', () => {
store.dispatch(increment(1));
});
increment5Btn.addEventListener('click', () => {
store.dispatch(increment(5));
});
decrementBtn.addEventListener('click', () => {
store.dispatch(decrement(1));
});
decrement5Btn.addEventListener('click', () => {
store.dispatch(decrement(5));
});
resetBtn.addEventListener('click', () => {
store.dispatch(reset());
});
setButton.addEventListener('click', () => {
const value = parseInt(setValueInput.value, 10);
if (!isNaN(value)) {
store.dispatch(setValue(value));
setValueInput.value = '';
}
});
6. Exercise Challenges
Once you have the basic counter working, try extending it with these challenges:
- Add Undo/Redo Functionality: Create new actions and update the reducer to allow undoing and redoing operations.
- Add a Counter History Graph: Use a simple chart library to visualize the counter's value over time.
- Add Multiple Counters: Modify your store to support multiple counters that can be added or removed.
- Add Persistence: Use
localStorage to persist the counter state between page refreshes.
Conclusion: When and Why to Use Redux
Redux provides a powerful way to manage state in complex applications, but it comes with a trade-off of additional complexity and boilerplate code. Here's a summary of when to consider using Redux:
Use Redux When:
- Your application has complex state logic that changes frequently
- The state needs to be used by many components at different levels
- You need a clear, predictable way to update state
- You want powerful dev tools for debugging state changes
- You need to implement time travel, undo/redo, or other state history features
- You have complex asynchronous actions affecting your state
Consider Alternatives When:
- Your application is simple with minimal state management needs
- You want to avoid the additional boilerplate
- State changes are simple and limited to a small part of your app
- You're just getting started with React and don't want to learn too many concepts at once
Redux is like a sophisticated traffic control system - it adds complexity but provides structure, predictability, and control that becomes invaluable as your application grows.
Remember that Redux is just one tool in your front-end development toolkit. The best developers know when to use Redux and when simpler solutions like React's built-in state management or the Context API might be more appropriate.
"Redux is not great for making simple things quickly. It's great for making really hard things possible." — Dan Abramov, Redux creator