DEV Community

A0mineTV
A0mineTV

Posted on

🚀Understanding React Context with a Task Management App

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;
}
Enter fullscreen mode Exit fullscreen mode

 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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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)