React Context + Dispatch Pattern
How to manage global state in React without loosing your mind.
If you’ve been using React for some time, you’ve probably passed state down the component tree more times than you can count. At first it’s manageable, but as the app grows, it quickly starts to feel messy.
You know there has to be a better way to organize things, but reaching for Redux or another library feels like too much for such a simple problem.
This is where React’s built-in tools can help more than you might expect.
In this post, we’ll look at how combining React Context with a dispatch-based reducer can help organize shared state in a simple and predictable way, without bringing in extra libraries or unnecessary complexity.
Why Not Just Use One?
“At this point, you might be thinking—why not just use Context, or reducers, or something simpler?”
It’s tempting to use only Context or only a reducer, but each one solves only half the problem.
Using Context alone usually looks like this:
<AuthContext.Provider value={{ user, setUser }}This works, but update logic slowly spreads across components. As the app grows, it becomes harder to track where and why state changes happen.
Using a reducer alone keeps updates clean:
const [state, dispatch] = useReducer(reducer, initialState);But this state stays local. As soon as multiple parts of the app need it, you’re back to passing props.
The combination fixes both issues.
How It All Comes Together
The idea is simple:
keep your state logic in one place, and make it available everywhere.
First, you create a reducer that defines how state should change.
function authReducer(state, action) {
switch (action.type) {
case "LOGIN":
return { ...state, isLoggedIn: true };
case "LOGOUT":
return { ...state, isLoggedIn: false };
default:
return state;
}
}Next, you create a Context and use the reducer inside a provider.
const AuthContext = createContext(null);
function AuthProvider({ children }) {
const [state, dispatch] = useReducer(authReducer, {
isLoggedIn: false,
});
return (
<AuthContext.Provider value={{ state, dispatch }}>
{children}
</AuthContext.Provider>
);
}Now wrap your app with this provider.
<AuthProvider>
<App />
</AuthProvider>Finally, any component can read state and dispatch actions, no props needed.
function LoginButton() {
const { state, dispatch } = useContext(AuthContext);
return (
<button onClick={() => dispatch({ type: "LOGIN" })}>
{state.isLoggedIn ? "Logout" : "Login"}
</button>
);
}That’s it.
Context shares the state, the reducer controls updates, and components stay clean and simple.
When This Pattern Really Helps
This pattern starts to shine when your app has shared state that’s updated from multiple places.
Multiple components need to read and update the same state
You have cart-like actions (add, remove, update, clear)
You’re dealing with complex UI interactions (like drag-and-drop)
Global UI state is involved (auth, modals, sidebar, theme, filters)
Redux or other libraries feel too heavy for the problem
