How to Split React Context by Responsibility

Separate a context value from its update function so readers only re-render for the part they use, then expose each side with a custom hook.

6 min read

To split React context by responsibility, create separate contexts for the value and its update function. A component that only dispatches actions then re-renders only when the dispatch function changes, not when the value changes.

The one-big-context problem

A context value often bundles data with the function that changes it, like a task list with a dispatch function. Every component that reads the bundle re-renders when the list changes, even a button that only adds tasks.

Separate the value from the updates

Create two contexts instead of one. The first holds the value, and the second holds the function that updates it.

App.jsxApp.jsx
import { createContext } from "react";
 
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

The value context holds the current task list. The dispatch context holds the function that sends actions, which stays the same object across renders.

Provide both from one component

Wrap the tree in both providers from a single TasksProvider component. The component owns the state and passes the value and dispatch separately.

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

The state still lives in TasksProvider. Its value and its updater reach the tree through two independent channels.

Add a custom hook for each side

Export hooks that read each context. The hooks hide the two contexts and give components a clear name for what they need.

App.jsxApp.jsx
import { useContext } from "react";
 
export function useTasks() {
  return useContext(TasksContext);
}
 
export function useTasksDispatch() {
  return useContext(TasksDispatchContext);
}

A component that renders the list calls useTasks. A component that only adds tasks calls useTasksDispatch, so it re-renders only if the dispatch function changes, which it normally does not.

Where the split pays off

The benefit appears when some readers only update and some only display. A task input that dispatches an action stops re-rendering when the task list changes, because it never subscribed to the value context. The list still re-renders, because it reads the value context directly.

What splitting does not solve

Components that all read the same value context still re-render together when that value changes. To reduce that, you need memoized values or smaller pieces of state, which the re-render article covers. See why context causes re-renders and how to reduce them.

When one context is still fine

Do not split a context just to split it. If every reader needs both the value and the update function, one context is simpler and perfectly clear. The split pays off when a meaningful group of readers only dispatches and would otherwise re-render for nothing.

What to learn next

For many providers in one tree, the same split scales cleanly when you combine multiple context providers.

Rune AI

Rune AI

Key Insights

  • Create one context for the value and one for the update function.
  • Provide both from a single provider component.
  • Export a custom hook for each context.
  • Readers that only dispatch skip value re-renders.
  • The dispatch function rarely changes identity.
RunePowered by Rune AI

Frequently Asked Questions

Why split a context into two?

A component that only dispatches actions does not need to re-render when the value changes. Separating the value from the update function lets each reader subscribe only to what it uses.

What are the two contexts typically called?

One context holds the value, such as a task list, and the other holds the update function, such as a dispatch function. Each gets a matching custom hook.

Does splitting remove all re-renders?

No. Every reader of the value context still re-renders when the value changes. Splitting helps components that only need the update function, which rarely changes.

Conclusion

Split a context by responsibility by creating separate contexts for the value and its update function, then expose each through a custom hook. Components that only dispatch actions stop re-rendering when the value changes, and the wiring stays in one provider.