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:
Step | What We Did |
---|---|
1 | Created a reducer function |
2 | Created two contexts (state + dispatch) |
3 | Wrapped app with the provider |
4 | Used 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)