Redux in React
Table of Contents + −
Redux is a predictable state container for JavaScript applications, commonly used with React. It helps you write applications that behave consistently and run in different environments.
Core Concepts
Redux is built on three fundamental principles:
- Single Source of Truth: The state of your whole application is stored in an object tree within a single store.
- State is Read-only: The only way to change the state is to emit an action, an object describing what happened.
- Changes are made with pure functions: To specify how the state tree is transformed by actions, you write pure reducers.
Basic Redux Setup
// Action Typesconst ADD_TODO = "ADD_TODO";const TOGGLE_TODO = "TOGGLE_TODO";
// Action Creatorsfunction addTodo(text) { return { type: ADD_TODO, text };}
function toggleTodo(index) { return { type: TOGGLE_TODO, index };}
// Reducerfunction todos(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; }}
// Storeconst store = createStore(todos);
Using Redux with React
// Providerimport { Provider } from "react-redux";
function App() { return ( <Provider store={store}> <TodoApp /> </Provider> );}
// Connecting Componentsimport { connect } from "react-redux";
function TodoList({ todos, toggleTodo }) { return ( <ul> {todos.map((todo, index) => ( <li key={index} onClick={() => toggleTodo(index)} style={{ textDecoration: todo.completed ? "line-through" : "none" }} > {todo.text} </li> ))} </ul> );}
function mapStateToProps(state) { return { todos: state };}
function mapDispatchToProps(dispatch) { return { toggleTodo: (index) => dispatch(toggleTodo(index)), };}
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
Redux Toolkit
Redux Toolkit is the official, opinionated, batteries-included toolset for efficient Redux development.
import { createSlice, configureStore } from "@reduxjs/toolkit";
// Sliceconst todosSlice = createSlice({ name: "todos", initialState: [], reducers: { addTodo: (state, action) => { state.push({ text: action.payload, completed: false }); }, toggleTodo: (state, action) => { const todo = state[action.payload]; if (todo) { todo.completed = !todo.completed; } }, },});
export const { addTodo, toggleTodo } = todosSlice.actions;
// Storeconst store = configureStore({ reducer: { todos: todosSlice.reducer, },});
Using Redux Toolkit with React Hooks
import { useSelector, useDispatch } from "react-redux";import { addTodo, toggleTodo } from "./todosSlice";
function TodoApp() { const todos = useSelector((state) => state.todos); const dispatch = useDispatch(); const [text, setText] = useState("");
const handleSubmit = (e) => { e.preventDefault(); if (text.trim()) { dispatch(addTodo(text)); setText(""); } };
return ( <div> <form onSubmit={handleSubmit}> <input value={text} onChange={(e) => setText(e.target.value)} placeholder="Add a todo" /> <button type="submit">Add Todo</button> </form> <ul> {todos.map((todo, index) => ( <li key={index} onClick={() => dispatch(toggleTodo(index))} style={{ textDecoration: todo.completed ? "line-through" : "none" }} > {todo.text} </li> ))} </ul> </div> );}
Middleware
Redux middleware provides a third-party extension point between dispatching an action and the moment it reaches the reducer.
// Logger Middlewareconst logger = (store) => (next) => (action) => { console.group(action.type); console.info("dispatching", action); const result = next(action); console.log("next state", store.getState()); console.groupEnd(); return result;};
// Apply Middlewareconst store = createStore(rootReducer, applyMiddleware(logger, thunk));
Best Practices
- Normalize State Shape: Keep your state normalized to avoid redundancy and simplify updates.
- Use Selectors: Use selectors to compute derived data and encapsulate state structure.
- Keep Actions Atomic: Each action should represent a single change to the state.
- Use Redux Toolkit: Redux Toolkit simplifies Redux setup and reduces boilerplate.
- Avoid Deeply Nested State: Flatten your state structure when possible.
- Use Immutable Update Patterns: Always update state immutably.
When to Use Redux
Redux is most useful when:
- You have large amounts of application state that many components need to access
- The state updates frequently
- The logic to update the state is complex
- You need to track state changes for debugging
For simpler applications, consider using React’s built-in useState and useContext hooks.
Conclusion
Redux provides a robust solution for managing complex state in React applications. While it has a learning curve, its predictable state updates and powerful developer tools make it a valuable tool for large-scale applications.