Manage your state with Context and useReducer
A common React challenge
Choosing a state management solution for a React application can quickly become overwhelming. The right choice depends largely on the size of your app and the complexity of the data you’re working with. While there are many popular libraries, such as MobX, Redux, and Zustand, not every project needs an external dependency.
In this article, we’ll explore one of the simplest and most effective patterns available in React today: combining the Context API with the useReducer hook. This approach is built entirely with React’s core features and works especially well for small to medium-sized applications.
At a high level, React applications usually deal with two kinds of state:
-
Local state, which lives inside a single component
-
Global state, which needs to be shared across multiple components in the component tree
Thanks to the introduction of the Context API and Hooks, managing global state no longer requires a third-party library. When state logic becomes more complex, useReducer provides a structured and predictable way to handle state transitions—much more suitable than useState for these scenarios.
Every library's approach tries to tackle the same problem in its own way. With the adoption of React Hooks API, one such option is to use a combination of useReducer hook and the Context API. In this post, we'll take a look at how to manage the global state in a React app using both of them.
There are two types of states to deal with in React apps. The first type is the local state that is used only within a React component. The second type is the global state that can be shared among multiple components within a React application's tree.
With the release of Context API as well as Hooks API, implementing a global state is possible without installing any additional state management library. The useReducer hook is a great way to manage complex state objects and state transitions. You may have seen or used useState to manage simple or local state in React apps.
How useReducer works
The useReducer hook is an alternative to useState. Its main advantage is that it scales better when dealing with complex state objects or multiple related state transitions.
Instead of directly setting state, useReducer relies on a reducer function that describes how the state should change in response to specific actions. The hook returns two values:
-
The current state
-
A
dispatchfunction used to trigger state updates
As stated in the official React documentation:
useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.
Creating the initial state
Let’s start with a simple example. Our state will consist of a single count property that we can increment or decrement.
const initialState = { count: 0 };
Defining action identifiers
Next, we define a set of action types. These identifiers allow the reducer to determine how the state should be updated.
const ACTIONS = {
INCREMENT: 'INCREMENT',
DECREMENT: 'DECREMENT',
};
Create the reducer function
The reducer function takes the current state and an action as arguments. Based on the action type, it returns a new state object.
In this example, we’re simply incrementing or decrementing the counter.
const reducer = (state, action) => {
switch (action.type) {
case ACTIONS.INCREMENT:
return { ...state, count: state.count + 1 };
case ACTIONS.DECREMENT:
return { ...state, count: state.count - 1 };
default:
return state;
}
};
Using a payload with actions
const []
// Dispatching an action with a payload
dispatch({
type: ACTIONS.INCREMENT
payload: { amount : 15, message: "Yay!" }
});
const reducer = (state, action) => {
// destructure the payload object
const { amount, message } = action.payload
switch (action.type) {
case ACTIONS.INCREMENT:
return {
...state,
count: state.count + amount,
message: message
};
// same as before, implement your logic here
default:
return state;
}
};
Creating the Context
Now let’s create the context that will hold our global store.
const AppContext = createContext();
Create the store Provider
The StoreProvider component is responsible for initializing the state and making it available to the rest of the application.
Inside this provider, we use useReducer to manage the state and useMemo to avoid unnecessary re-renders.
const StoreProvider = ({ children }) => {
if (!children) throw new Error('StateProvider! no children given');
// Access to state and dispatch function
const [state, dispatch] = useReducer(reducer, initialState);
const store = useMemo(() => ({ state, dispatch }), [state, dispatch]);
return <AppContext.Provider value={store}>{children}</AppContext.Provider>;
};
Creating a custom hook to access the store
To make consuming the context easier and safer, we can create a custom hook. This hook ensures that the context is only used within the provider.
const useStore = () => {
const context = useContext(AppContext);
if (!context) throw new Error('useStore must be used within StateProvider!');
return context;
};
Exporting what you need
export { useStore, StoreProvider };
Demo
You can find a working example on CodeSandbox here