Table of Contents
- What is React Redux?
- Core Concepts of Redux
- Store
- Actions
- Reducers
- Dispatch
- Selectors
- Setting Up React Redux
- Prerequisites
- Installing Dependencies
- Initializing the Redux Store
- Implementing Redux in a React App: A Practical Example
- Creating a Slice with Redux Toolkit
- Connecting Components with Hooks
- Testing the Implementation
- Advanced Redux Concepts
- Middleware (Redux Thunk)
- Async Operations with Redux Toolkit
- Memoization with Selectors (Reselect)
- Debugging with Redux DevTools
- When to Use React Redux
- Conclusion
- References
What is React Redux?
Redux itself is a standalone state management library inspired by Flux and functional programming principles. It provides a centralized store for managing application state, with strict rules for how state can be updated. However, Redux is not tied to React—it can be used with any UI library (e.g., Vue, Angular).
React Redux is the official package that connects Redux to React applications. It provides tools to:
- Inject the Redux store into React components.
- Access state from the store in components.
- Dispatch actions to update the store.
In short, React Redux acts as a bridge, allowing React components to interact with the Redux store seamlessly.
Core Concepts of Redux
To use React Redux effectively, you must first understand Redux’s core principles. Let’s break them down:
1. Store
The store is the single source of truth for your application’s state. It holds the entire state tree and provides methods to access and update state. With Redux Toolkit (the modern way to write Redux), creating a store is simplified, but conceptually, it’s still a container for state.
2. Actions
Actions are plain JavaScript objects that describe what happened in the application. They are the only way to trigger state changes. Every action must have a type field (a string describing the action) and may include a payload (data associated with the action).
Example action:
{ type: 'todos/addTodo', payload: 'Learn React Redux' }
3. Reducers
Reducers are pure functions that specify how the state changes in response to an action. They take the current state and an action as inputs and return a new state object (never mutate the existing state!).
Example reducer (simplified):
function todosReducer(state = [], action) {
switch (action.type) {
case 'todos/addTodo':
return [...state, action.payload]; // Return new state
default:
return state; // Return current state for unknown actions
}
}
4. Dispatch
Dispatch is a method provided by the store to send actions to reducers. When you dispatch an action, the store runs the reducer with the current state and the action, then updates the state.
Example dispatch:
store.dispatch({ type: 'todos/addTodo', payload: 'Learn React Redux' });
5. Selectors
Selectors are functions that extract specific pieces of state from the store. They help decouple components from the shape of the state tree, making code more maintainable. With libraries like Reselect, selectors can also memoize results for better performance.
Setting Up React Redux
Let’s walk through setting up a React app with Redux. We’ll use Redux Toolkit (RTK), the official recommended approach for writing Redux logic, as it simplifies common tasks like creating reducers and handling async operations.
Prerequisites
- Node.js (v14+) and npm/yarn
- Basic knowledge of React
Step 1: Create a React App
If you don’t already have a React app, create one using Create React App:
npx create-react-app redux-demo
cd redux-demo
Step 2: Install Dependencies
Install Redux Toolkit and React Redux:
npm install @reduxjs/toolkit react-redux
Step 3: Initialize the Redux Store
With Redux Toolkit, creating a store is straightforward. Create a store.js file in the src folder:
// src/store.js
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: {
// We'll add reducers here later
},
});
The configureStore function automatically sets up middleware (like Redux Thunk for async logic) and integrates with Redux DevTools for debugging.
Step 4: Provide the Store to React
Wrap your root component with Provider from React Redux to make the store available to all components. Update src/index.js:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './store';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
Implementing Redux in a React App: A Practical Example
Let’s build a simple todo app to demonstrate React Redux in action. We’ll use Redux Toolkit’s createSlice to define our state logic, then connect components to the store.
Step 1: Create a Todo Slice
A “slice” is a collection of Redux logic (reducers and actions) for a specific feature (e.g., todos). Create src/features/todos/todoSlice.js:
// src/features/todos/todoSlice.js
import { createSlice } from '@reduxjs/toolkit';
// Initial state
const initialState = {
items: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
};
// Create a slice
const todoSlice = createSlice({
name: 'todos', // Name used in action types
initialState,
reducers: {
// Reducer for adding a todo
todoAdded: (state, action) => {
// Redux Toolkit uses Immer, so we can "mutate" state directly (it's a draft)
state.items.push({
id: Date.now(),
text: action.payload,
completed: false,
});
},
// Reducer for toggling a todo
todoToggled: (state, action) => {
const todo = state.items.find(todo => todo.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
},
});
// Export actions generated by createSlice
export const { todoAdded, todoToggled } = todoSlice.actions;
// Export the reducer to add to the store
export default todoSlice.reducer;
Key points:
createSliceauto-generates action creators (e.g.,todoAdded) from reducer names.- Immer library allows “mutating” state in reducers (it converts mutations to immutable updates).
Step 2: Add the Slice Reducer to the Store
Update src/store.js to include the todo slice reducer:
// src/store.js
import { configureStore } from '@reduxjs/toolkit';
import todoReducer from './features/todos/todoSlice';
export const store = configureStore({
reducer: {
todos: todoReducer, // State for todos will live under store.todos
},
});
Step 3: Create a Todo Component
Build a component to interact with the todo state. Create src/features/todos/TodoList.js:
// src/features/todos/TodoList.js
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { todoAdded, todoToggled } from './todoSlice';
export default function TodoList() {
const [text, setText] = useState('');
const dispatch = useDispatch(); // Hook to dispatch actions
const todos = useSelector(state => state.todos.items); // Hook to access state
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
dispatch(todoAdded(text)); // Dispatch the todoAdded action
setText(''); // Clear input
};
return (
<div>
<h2>Todo List</h2>
<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>
<ul>
{todos.map(todo => (
<li
key={todo.id}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
onClick={() => dispatch(todoToggled(todo.id))}
>
{todo.text}
</li>
))}
</ul>
</div>
);
}
Here, we use two React Redux hooks:
useSelector: Accesses state from the store (e.g.,state.todos.items).useDispatch: Returns a function to dispatch actions (e.g.,dispatch(todoAdded(text))).
Step 4: Update the App Component
Import and render TodoList in src/App.js:
// src/App.js
import './App.css';
import TodoList from './features/todos/TodoList';
function App() {
return (
<div className="App">
<h1>React Redux Todo App</h1>
<TodoList />
</div>
);
}
export default App;
Step 5: Test the App
Run the app with npm start. You should see:
- An input to add todos.
- A list of todos that can be toggled by clicking.
Advanced Redux Concepts
Middleware: Handling Async Logic with Redux Thunk
Redux reducers are pure functions and cannot handle async operations (e.g., API calls). For this, we use middleware like Redux Thunk (included by default in Redux Toolkit).
Example: Fetch todos from an API using createAsyncThunk:
// In todoSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
// Async thunk to fetch todos
export const fetchTodos = createAsyncThunk(
'todos/fetchTodos', // Action type prefix
async () => {
const response = await axios.get('https://jsonplaceholder.typicode.com/todos');
return response.data.slice(0, 5); // Return first 5 todos
}
);
const todoSlice = createSlice({
name: 'todos',
initialState: { items: [], status: 'idle', error: null },
reducers: { /* ... */ },
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;
});
},
});
Dispatch fetchTodos in a component to load data asynchronously.
Memoized Selectors with Reselect
For complex state extraction, use reselect to create memoized selectors that only recalculate when their inputs change:
// src/features/todos/selectors.js
import { createSelector } from '@reduxjs/toolkit';
const selectTodosState = (state) => state.todos;
export const selectTodoItems = createSelector(
[selectTodosState],
(todos) => todos.items
);
export const selectCompletedTodos = createSelector(
[selectTodoItems],
(items) => items.filter(todo => todo.completed)
);
Debugging with Redux DevTools
Redux DevTools (built into Chrome/Firefox) let you inspect state changes, replay actions, and debug async logic. Redux Toolkit enables this by default.
When to Use React Redux
Redux is powerful, but it’s not always necessary. Use it when:
- Your app has complex state (e.g., nested data, many state updates).
- Multiple components need access to the same state.
- You need predictable state updates (e.g., for debugging or testing).
- You want a centralized state for easier collaboration.
Avoid Redux if:
- Your app is small with simple state (use React Context + useReducer instead).
- State is local to a single component (use useState).
Conclusion
React Redux, paired with Redux Toolkit, provides a robust solution for managing state in large React applications. By centralizing state, enforcing predictable updates, and simplifying async logic, it helps teams build scalable, maintainable apps.
Key takeaways:
- Use Redux Toolkit for modern, concise Redux code.
- Leverage
useSelectoranduseDispatchhooks to connect components. - Use middleware like Redux Thunk for async operations.
- Reserve Redux for apps with complex state—avoid overengineering small projects.