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.

Postgres on Neon - Get the Free Plan

No credit card required. The database you love, on a serverless platform designed to help you build faster.

Get Postgres on Neon

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

đź‘‹ Kindness is contagious

Immerse yourself in a wealth of knowledge with this piece, supported by the inclusive DEV Community—every developer, no matter where they are in their journey, is invited to contribute to our collective wisdom.

A simple “thank you” goes a long way—express your gratitude below in the comments!

Gathering insights enriches our journey on DEV and fortifies our community ties. Did you find this article valuable? Taking a moment to thank the author can have a significant impact.

Okay