Understanding React Context with a Task Management App
React Context provides a way to share values like state and dispatch across the component tree without prop drilling. In this article, we'll walk through implementing React Context in a Task Management App to manage tasks efficiently.
Why React Context?
Prop drilling is a common problem when passing props through multiple layers of components. React Context helps solve this by allowing you to define a global state that any component can access directly.
Our Project: Task Management App
We'll build a simple Task Management App where users can:
- Add tasks
- Edit tasks
- Mark tasks as complete/incomplete
- Delete tasks
Let's dive into the implementation.
Setting Up Context
First, create a context to manage our tasks.
Define the Task Type and Context
In TaskContext.tsx
, define the TaskType
, initial state, and context setup.
import React, { createContext, useReducer, PropsWithChildren } from "react";
export interface TaskType {
id: number;
text: string;
done: boolean;
}
type Action =
| { type: "added"; id: number; text: string }
| { type: "changed"; task: TaskType }
| { type: "deleted"; id: number };
interface TaskContextType {
tasks: TaskType[];
dispatch: React.Dispatch<Action>;
}
const initialState: TaskType[] = [];
const TaskContext = createContext<TaskContextType | undefined>(undefined);
function taskReducer(tasks: TaskType[], action: Action): TaskType[] {
switch (action.type) {
case "added":
return [...tasks, { id: action.id, text: action.text, done: false }];
case "changed":
return tasks.map((task) =>
task.id === action.task.id ? action.task : task
);
case "deleted":
return tasks.filter((task) => task.id !== action.id);
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
}
export function TaskProvider({ children }: PropsWithChildren) {
const [tasks, dispatch] = useReducer(taskReducer, initialState);
return (
<TaskContext.Provider value={{ tasks, dispatch }}>
{children}
</TaskContext.Provider>
);
}
export function useTaskContext() {
const context = React.useContext(TaskContext);
if (!context) {
throw new Error("useTaskContext must be used within a TaskProvider");
}
return context;
}
 Using Context in Components
Now that the context is ready, let’s use it in our app.
Add Task Component
import React, { useState } from "react";
import { useTaskContext } from "./TaskContext";
export function AddTask() {
const { dispatch } = useTaskContext();
const [text, setText] = useState("");
const handleAddTask = () => {
if (text.trim()) {
dispatch({ type: "added", id: Date.now(), text });
setText(""); // Clear the input
}
};
return (
<div>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add a new task"
/>
<button onClick={handleAddTask}>Add Task</button>
</div>
);
}
Task Component
import React, { useState } from "react";
import { TaskType, useTaskContext } from "./TaskContext";
interface TaskProps {
task: TaskType;
}
export function Task({ task }: TaskProps) {
const { dispatch } = useTaskContext();
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(task.text);
const handleSave = () => {
dispatch({ type: "changed", task: { ...task, text: editText } });
setIsEditing(false);
};
return (
<div>
<input
type="checkbox"
checked={task.done}
onChange={() =>
dispatch({
type: "changed",
task: { ...task, done: !task.done },
})
}
/>
{isEditing ? (
<>
<input
value={editText}
onChange={(e) => setEditText(e.target.value)}
/>
<button onClick={handleSave}>Save</button>
</>
) : (
<>
{task.text}
<button onClick={() => setIsEditing(true)}>Edit</button>
</>
)}
<button onClick={() => dispatch({ type: "deleted", id: task.id })}>
Delete
</button>
</div>
);
}
Integrating Components
Finally, bring everything together in App.tsx
.
import React from "react";
import { TaskProvider } from "./TaskContext";
import { AddTask } from "./AddTask";
import { Task } from "./Task";
export default function App() {
return (
<TaskProvider>
<h1>Task Management App</h1>
<AddTask />
{/* Use the context to map tasks */}
<TaskList />
</TaskProvider>
);
}
function TaskList() {
const { tasks } = useTaskContext();
return (
<div>
{tasks.map((task) => (
<Task key={task.id} task={task} />
))}
</div>
);
}
Conclusion
By using React Context, we eliminated the need for prop drilling in our Task Management App. The TaskProvider
encapsulates state management and can easily be expanded for more features.
This structure is scalable, maintainable, and ensures clean separation of concerns. React Context combined with useReducer
is a powerful pattern for managing global state in React apps.
Top comments (0)