Of course! Let's break it down step-by-step with a real-life example
Suppose we are building a simple Todo app where you can add and toggle tasks.


Step 1: Create a Reducer

The reducer will manage the tasks state.

// tasksReducer.js
export function tasksReducer(tasks, action) {
  switch (action.type) {
    case "added": {
      return [...tasks, { id: action.id, text: action.text, done: false }];
    }
    case "toggled": {
      return tasks.map((task) =>
        task.id === action.id ? { ...task, done: !task.done } : task
      );
    }
    default: {
      throw new Error("Unknown action: " + action.type);
    }
  }
}

✅ Here tasksReducer takes the current tasks and an action and returns the new tasks list.


Step 2: Create Two Contexts (for state and dispatch)

// TasksContext.js
import { createContext, useContext, useReducer } from "react";
import { tasksReducer } from "./tasksReducer";
 
// 1. Create contexts
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);
 
// 2. Create a provider component
export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(tasksReducer, []);
 
  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        {children}
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}
 
// 3. Create custom hooks
export function useTasks() {
  return useContext(TasksContext);
}
 
export function useTasksDispatch() {
  return useContext(TasksDispatchContext);
}

TasksProvider wraps children and gives them tasks and dispatch.
useTasks() lets you read tasks.
useTasksDispatch() lets you dispatch actions.


Step 3: Use the Provider at a Higher Level

// App.js
import { TasksProvider } from "./TasksContext";
import TaskList from "./TaskList";
import AddTask from "./AddTask";
 
export default function App() {
  return (
    <TasksProvider>
      <h1>Todo App</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}

✅ Here the whole app is inside TasksProvider, so any child component can use tasks state and dispatch.


Step 4: Use useTasks and useTasksDispatch in Components

AddTask.js

import { useTasksDispatch } from "./TasksContext";
import { useState } from "react";
 
export default function AddTask() {
  const [text, setText] = useState("");
  const dispatch = useTasksDispatch();
 
  function handleAddTask() {
    dispatch({
      type: "added",
      id: Date.now(),
      text: text,
    });
    setText("");
  }
 
  return (
    <>
      <input
        placeholder="Add task"
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <button onClick={handleAddTask}>Add</button>
    </>
  );
}

AddTask uses only dispatch to add a new task.


TaskList.js

import { useTasks } from "./TasksContext";
import TaskItem from "./TaskItem";
 
export default function TaskList() {
  const tasks = useTasks();
 
  return (
    <ul>
      {tasks.map((task) => (
        <TaskItem key={task.id} task={task} />
      ))}
    </ul>
  );
}

TaskList uses useTasks() to read tasks.


TaskItem.js

import { useTasksDispatch } from "./TasksContext";
 
export default function TaskItem({ task }) {
  const dispatch = useTasksDispatch();
 
  return (
    <li>
      {task.text}
      <button
        onClick={() => {
          dispatch({
            type: "toggled",
            id: task.id,
          });
        }}
      >
        {task.done ? "Undo" : "Done"}
      </button>
    </li>
  );
}

TaskItem uses dispatch to toggle a task.


Final Structure:

src/
├── App.js
├── tasksReducer.js
├── TasksContext.js
├── AddTask.js
├── TaskList.js
├── TaskItem.js

Recap in super simple words:

StepWhat We Did
1Created a reducer function
2Created two contexts (state + dispatch)
3Wrapped app with the provider
4Used custom hooks to access and update state easily

Would you also like me to draw a simple diagram showing how the Provider and components connect? 🎯
It'll make it even clearer! 🚀 (just say yes)