Introduction to State Management
As React applications grow, managing state becomes increasingly complex. Component state works well for isolated data, but presents challenges when state needs to be:
- Shared across multiple components
- Updated from different locations in your application
- Cached, persisted, or synchronized
Think of state management like managing inventory in a large store. When the store is small with few products (components), a simple notepad (local state) works fine. But as the store grows to multiple departments with thousands of products, you need a centralized inventory system (global state management) to track everything efficiently.
Types of State Management Solutions
- Component State: Using React's built-in useState, useReducer hooks
- Context API: React's built-in system for prop drilling prevention
- State Management Libraries: Redux, MobX, Zustand, Recoil, Jotai, etc.
What is Redux?
Redux is a predictable state container for JavaScript applications. It helps you write applications that behave consistently across different environments and are easy to test.
Redux was inspired by Flux architecture and functional programming concepts. It was created by Dan Abramov and Andrew Clark in 2015 and quickly became the most popular state management solution for React applications.
When Do You Need Redux?
Redux is most beneficial when:
- Your application has a large amount of state used in many places
- State updates are complex and happen frequently
- Your application has a medium to large codebase with many developers
- You need to track how state changes over time (time-travel debugging)
- You want a predictable, consistent pattern for state updates
Real-world examples of applications that benefit from Redux:
- Social media platforms with complex user interactions
- E-commerce sites with shopping carts, user preferences, and product filters
- Collaborative tools with real-time updates and undo/redo functionality
- Dashboard applications with multiple data sources and visualizations
The Three Principles of Redux
Redux is built on three fundamental principles:
1. Single Source of Truth
The entire state of your application is stored in a single JavaScript object called the store.
Analogy: Think of the Redux store like a bank's central database. All your account information is stored in one secure location, rather than scattered across multiple systems. This makes it easier to track, audit, and manage your financial data.
// Example of a Redux store state object
{
user: {
id: "u123",
name: "Jane Doe",
email: "jane@example.com",
preferences: {
theme: "dark",
notifications: true
}
},
products: {
items: [...],
isLoading: false,
error: null
},
cart: {
items: [...],
totalAmount: 129.99
}
}
2. State is Read-Only
The only way to change state is to emit an action, which is a plain JavaScript object describing what happened.
Analogy: Imagine a library where books (state) can't be modified directly. Instead, you submit a change request form (action) to the librarian, who then makes the appropriate modifications according to the library's rules.
// Examples of Redux actions
// Action to add item to cart
{
type: 'ADD_TO_CART',
payload: {
productId: 'p123',
name: 'Wireless Headphones',
price: 79.99,
quantity: 1
}
}
// Action to toggle theme
{
type: 'TOGGLE_THEME'
}
// Action to update user profile
{
type: 'UPDATE_USER_PROFILE',
payload: {
name: 'Jane Smith',
email: 'jane.smith@example.com'
}
}
3. Changes are Made with Pure Functions
To specify how the state tree is transformed by actions, you write pure reducers - functions that take the previous state and an action, and return the next state.
Analogy: Reducers are like pastry chefs following a strict recipe. Given the same ingredients (previous state and action) and recipe (reducer logic), they will always produce the same pastry (next state) without affecting anything else in the kitchen.
// Example of a Redux reducer
function cartReducer(state = { items: [], totalAmount: 0 }, action) {
switch (action.type) {
case 'ADD_TO_CART':
const newItem = action.payload;
const existingItemIndex = state.items.findIndex(
item => item.productId === newItem.productId
);
if (existingItemIndex >= 0) {
// Item already exists, update quantity
const updatedItems = [...state.items];
updatedItems[existingItemIndex] = {
...updatedItems[existingItemIndex],
quantity: updatedItems[existingItemIndex].quantity + newItem.quantity
};
return {
...state,
items: updatedItems,
totalAmount: state.totalAmount + (newItem.price * newItem.quantity)
};
} else {
// Add new item
return {
...state,
items: [...state.items, newItem],
totalAmount: state.totalAmount + (newItem.price * newItem.quantity)
};
}
case 'REMOVE_FROM_CART':
const productId = action.payload;
const itemToRemove = state.items.find(item => item.productId === productId);
if (!itemToRemove) return state;
return {
...state,
items: state.items.filter(item => item.productId !== productId),
totalAmount: state.totalAmount - (itemToRemove.price * itemToRemove.quantity)
};
default:
return state;
}
}
Redux Core Concepts
Actions
Actions are payloads of information that send data from your application to your store. They are the only source of information for the store.
Key characteristics of actions:
- Plain JavaScript objects
- Must have a
typeproperty (typically a string constant) - Can contain additional data in any format (commonly in a
payloadproperty) - Should be descriptive and event-oriented (e.g.,
USER_LOGGED_IN)
Action Creators
Action creators are functions that create and return action objects. They encapsulate the action creation logic and make dispatching actions more convenient.
// Action creator
function addToCart(product, quantity = 1) {
return {
type: 'ADD_TO_CART',
payload: {
productId: product.id,
name: product.name,
price: product.price,
quantity
}
};
}
// Usage
dispatch(addToCart(product, 2));
Reducers
Reducers specify how the application's state changes in response to actions. A reducer is a pure function that takes the previous state and an action, and returns the next state.
Key characteristics of reducers:
- Pure functions (no side effects, same output for same input)
- Never modify the state argument (create a new state object instead)
- Return the original state for unrecognized actions
- Handle each action type with a separate case in a switch statement (or similar logic)
Store
The Redux store is the object that brings actions and reducers together. The store has the following responsibilities:
- Holds the application state
- Allows access to state via
getState() - Allows state to be updated via
dispatch(action) - Registers listeners via
subscribe(listener) - Handles unregistering of listeners
import { createStore } from 'redux';
import rootReducer from './reducers';
// Create the Redux store
const store = createStore(rootReducer);
// Log the initial state
console.log(store.getState());
// Subscribe to state changes
const unsubscribe = store.subscribe(() => {
console.log('State updated:', store.getState());
});
// Dispatch actions
store.dispatch(addToCart(product));
store.dispatch(updateUserProfile(userInfo));
// Unsubscribe to stop listening for state updates
unsubscribe();
Redux Data Flow
Redux follows a strict unidirectional data flow, making state changes predictable and traceable:
- Event occurs in the UI (button click, form submission, API response)
- Action is dispatched to describe what happened
- Reducer processes the action and creates a new state
- Store updates the state and notifies all listeners
- UI updates to reflect the new state
This unidirectional flow has several advantages:
- Predictable state updates (same actions always produce same results)
- Maintainable code with clear responsibility separation
- Easier debugging and testing
- Ability to track every state change
Real-world analogy: Think of Redux's data flow like an efficient restaurant. The customer (UI) places an order (action), the waiter (dispatcher) takes it to the kitchen, the chef (reducer) prepares the meal according to the recipe, the completed dish (new state) is delivered back to the customer, and the customer enjoys their meal (UI updates).
Setting Up a Basic Redux Store
Let's walk through setting up a basic Redux store for a counter application:
Step 1: Define Action Types
// actionTypes.js
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const RESET = 'RESET';
export const SET_VALUE = 'SET_VALUE';
Step 2: Create Action Creators
// actions.js
import { INCREMENT, DECREMENT, RESET, SET_VALUE } from './actionTypes';
export const increment = () => ({
type: INCREMENT
});
export const decrement = () => ({
type: DECREMENT
});
export const reset = () => ({
type: RESET
});
export const setValue = (value) => ({
type: SET_VALUE,
payload: value
});
Step 3: Create Reducer
// reducer.js
import { INCREMENT, DECREMENT, RESET, SET_VALUE } from './actionTypes';
const initialState = {
count: 0
};
function counterReducer(state = initialState, action) {
switch (action.type) {
case INCREMENT:
return {
...state,
count: state.count + 1
};
case DECREMENT:
return {
...state,
count: state.count - 1
};
case RESET:
return {
...state,
count: 0
};
case SET_VALUE:
return {
...state,
count: action.payload
};
default:
return state;
}
}
export default counterReducer;
Step 4: Create and Configure the Store
// store.js
import { createStore } from 'redux';
import counterReducer from './reducer';
const store = createStore(
counterReducer,
// Optional: Add Redux DevTools Extension support
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
export default store;
Step 5: Use the Store
// index.js
import store from './store';
import { increment, decrement, reset, setValue } from './actions';
// Log initial state
console.log('Initial state:', store.getState());
// Subscribe to state changes
const unsubscribe = store.subscribe(() => {
console.log('Updated state:', store.getState());
});
// Dispatch actions
store.dispatch(increment());
// => { count: 1 }
store.dispatch(increment());
// => { count: 2 }
store.dispatch(decrement());
// => { count: 1 }
store.dispatch(setValue(100));
// => { count: 100 }
store.dispatch(reset());
// => { count: 0 }
// Stop listening to state updates
unsubscribe();
Practice Activity
Build a Todo List with Redux
Create a simple todo list application using Redux. The application should support:
- Adding new todos
- Toggling todo completion status
- Filtering todos (All, Active, Completed)
- Deleting todos
Follow these steps:
- Define action types (ADD_TODO, TOGGLE_TODO, DELETE_TODO, SET_FILTER)
- Create action creators for each action type
- Create reducers for todos and filter state
- Combine reducers using combineReducers
- Set up the Redux store
- Test your store by dispatching actions and logging the state
Start with this skeleton code:
// actionTypes.js
export const ADD_TODO = 'ADD_TODO';
export const TOGGLE_TODO = 'TOGGLE_TODO';
export const DELETE_TODO = 'DELETE_TODO';
export const SET_FILTER = 'SET_FILTER';
// Define filter constants
export const FILTERS = {
ALL: 'ALL',
ACTIVE: 'ACTIVE',
COMPLETED: 'COMPLETED'
};
// actions.js
// TODO: Create action creators
// reducers.js
// TODO: Create and combine reducers
// store.js
// TODO: Create and configure the store
Key Takeaways
- Redux provides a predictable state container for JavaScript applications
- The three principles of Redux: single source of truth, state is read-only, and changes are made with pure functions
- Core Redux concepts include actions, reducers, and the store
- Redux follows a strict unidirectional data flow
- Redux is most beneficial for applications with complex state management requirements
- Redux promotes predictability, maintainability, and testability