Skip to main content

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 and filter.

🎯 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.

Further Reading