Combining Context with `useReducer` for Complex State (Part 2): A Practical Example #103
📖 Introduction
Following our introduction to the powerful pattern of combining Context with useReducer
, this article provides a more complex, practical example. We will build a simple to-do list application to demonstrate how this pattern can be used to manage a list of items.
📚 Prerequisites
Before we begin, please ensure you have a solid grasp of the following concepts:
- All concepts from Part 1 of this series.
- JavaScript array methods like
map
andfilter
.
🎯 Article Outline: What You'll Master
In this article, you will learn:
- ✅ Creating a To-Do List Reducer: How to write a reducer to handle adding, removing, and toggling to-do items.
- ✅ Providing the To-Do List Context: How to create a provider component for our to-do list.
- ✅ Building a To-Do List Application: A step-by-step guide to building a fully functional to-do list application.
🧠 Section 1: The Core Concepts of a To-Do List with useReducer
and Context
A to-do list is a great example of a feature that can benefit from the useReducer
and Context pattern. The state is an array of to-do items, and there are multiple actions that can be performed on that state (add, remove, toggle).
By using a reducer, we can centralize all of our state update logic in one place. By using context, we can make the list of to-dos and the dispatch
function available to any component that needs them, without having to pass props down through the component tree.
💻 Section 2: Deep Dive - Implementation and Walkthrough
Let's build our to-do list application.
2.1 - The TodoContext
First, let's create a TodoContext.js
file.
// TodoContext.js
import React, { createContext, useReducer } from 'react';
export const TodoContext = createContext();
const initialState = {
todos: [],
};
function reducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
todos: [...state.todos, { id: Date.now(), text: action.payload, completed: false }],
};
case 'TOGGLE_TODO':
return {
todos: state.todos.map(todo =>
todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
),
};
case 'REMOVE_TODO':
return {
todos: state.todos.filter(todo => todo.id !== action.payload),
};
default:
throw new Error();
}
}
export function TodoProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<TodoContext.Provider value={{ state, dispatch }}>
{children}
</TodoContext.Provider>
);
}
2.2 - The TodoList
and TodoItem
Components
Now, let's create the components to display our to-do list.
// TodoList.js
import React, { useContext } from 'react';
import { TodoContext } from './TodoContext';
import TodoItem from './TodoItem';
function TodoList() {
const { state } = useContext(TodoContext);
return (
<ul>
{state.todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
export default TodoList;
// TodoItem.js
import React, { useContext } from 'react';
import { TodoContext } from './TodoContext';
function TodoItem({ todo }) {
const { dispatch } = useContext(TodoContext);
return (
<li
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
onClick={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}
>
{todo.text}
<button onClick={() => dispatch({ type: 'REMOVE_TODO', payload: todo.id })}>
X
</button>
</li>
);
}
export default TodoItem;
2.3 - The AddTodoForm
Component
Let's create a form to add new to-do items.
// AddTodoForm.js
import React, { useState, useContext } from 'react';
import { TodoContext } from './TodoContext';
function AddTodoForm() {
const [text, setText] = useState('');
const { dispatch } = useContext(TodoContext);
const handleSubmit = (event) => {
event.preventDefault();
dispatch({ type: 'ADD_TODO', payload: text });
setText('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">Add Todo</button>
</form>
);
}
export default AddTodoForm;
2.4 - The App.js
File
Finally, let's put it all together in App.js
.
// App.js
import React from 'react';
import { TodoProvider } from './TodoContext';
import TodoList from './TodoList';
import AddTodoForm from './AddTodoForm';
function App() {
return (
<TodoProvider>
<h1>Todo List</h1>
<AddTodoForm />
<TodoList />
</TodoProvider>
);
}
export default App;
💡 Conclusion & Key Takeaways
In this article, we've built a complete to-do list application using the Context API and useReducer
. We've seen how this powerful pattern can be used to manage complex state in a clean and scalable way.
Let's summarize the key takeaways:
- The
useReducer
hook is a great choice for managing state with multiple actions. - The Context API allows you to provide the state and dispatch function to any component that needs them.
- This pattern is a great way to build complex, data-driven applications.
Challenge Yourself: To solidify your understanding, try to add an "edit" feature to the to-do list that allows you to edit the text of a to-do item.
➡️ Next Steps
You now have a solid understanding of how to use the Context API and useReducer
to manage complex state. In the next article, "Context API Pitfalls", we will discuss some of the common pitfalls of the Context API and how to avoid them.
Thank you for your dedication. Stay curious, and happy coding!
glossary
- To-Do List: A list of tasks that need to be completed.
- State Management: The process of managing the state of an application.