How to Combine useReducer and Context

Combine useReducer with context to share reducer state and dispatch down the tree without prop drilling, using a provider and small custom hooks.

6 min read

Combining useReducer and context turns local reducer state into state any component below can read and update. The reducer still owns all update logic, while context removes the prop drilling between distant parts of the tree. The result is one provider that manages the state and exposes it to the whole subtree.

Create two contexts

Create one context for the value and a second context for the dispatch function. Export both from a separate file so consumers import the same objects.

index.jsindex.js
import { createContext } from "react";
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

Passing null as the default means a consumer that reads without a provider gets null instead of crashing on undefined. The real values come from the provider you build next.

Why two contexts instead of one

Splitting state and dispatch matters for re-renders. A component that only needs to dispatch does not care when the tasks change, so it should not re-render with every update. Two contexts let a reader subscribe to the value while a writer only subscribes to the dispatch function.

Provide state and dispatch

The reducer itself does not change when you add context. It still takes the current state and an action, then returns the next state. The contexts, reducer, provider, and hooks can all live in one TasksContext file.

index.jsindex.js
function tasksReducer(tasks, action) {
  switch (action.type) {
    case "added": return [...tasks, { id: action.id, text: action.text }];
    case "removed": return tasks.filter((task) => task.id !== action.id);
    default: return tasks;
  }
}

A provider component calls useReducer and passes the state and dispatch into the two contexts, with children rendered in between.

App.jsxApp.jsx
export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
  return (
    <TasksContext value={tasks}>
      <TasksDispatchContext value={dispatch}>
        {children}
      </TasksDispatchContext>
    </TasksContext>
  );
}

In React 19 you render the context object directly as a provider, as shown. Older versions use TasksContext.Provider with the same value prop. The provider is the only component that calls useReducer, so the tasks state has one clear owner.

Read and update from anywhere

Any component below the provider reads what it needs with useContext. A button that adds a task only needs the dispatch function, so it ignores the tasks list entirely. A list component would read TasksContext and never import dispatch.

App.jsxApp.jsx
import { useContext } from "react";
function AddTask() {
  const dispatch = useContext(TasksDispatchContext);
  function add() {
    dispatch({ type: "added", id: 3, text: "Pack" });
  }
  return <button onClick={add}>Add task</button>;
}

Clicking the button dispatches through context. The reducer in the provider updates the tasks, and only components that read TasksContext re-render. The AddTask component subscribes only to the dispatch context, so a change to the list leaves it untouched.

Hide the wiring behind custom hooks

Export small hooks that wrap each useContext call. Consumers then call useTasks or useTasksDispatch instead of importing the context objects directly.

index.jsindex.js
export function useTasks() {
  return useContext(TasksContext);
}
export function useTasksDispatch() {
  return useContext(TasksDispatchContext);
}

These hooks change no behavior, but they keep the context objects private and give you one place to add checks later, such as throwing when a provider is missing. See create, provide, and read context for the context fundamentals, and useReducer explained with examples for the reducer half.

Rune AI

Rune AI

Key Insights

  • useReducer keeps state and dispatch in one provider.
  • Context removes prop drilling for distant components.
  • Split state and dispatch into two contexts.
  • Export custom hooks to read them safely.
  • The state still lives in the provider, not in context.
RunePowered by Rune AI

Frequently Asked Questions

Do I need two separate contexts?

Splitting state and dispatch into two contexts is the common pattern. It lets readers of state skip re-renders that only affect dispatch, and it keeps each concern explicit.

Where does the state live when I combine them?

The state still lives in the provider component that calls useReducer. Context only shares the current value and the dispatch function with the tree below.

Can I use one context for both state and dispatch?

Yes, but a combined object changes identity every render, so every consumer re-renders. Two contexts avoid that and are easier to split later.

Conclusion

Combining useReducer and context keeps all update logic in one reducer while letting any component read or dispatch without prop drilling. Create two contexts, provide them from one provider, and expose the result through small custom hooks.