Manage your state with Context and useReducer
Choosing a state management library to manage and handle a global state in a React app can be very tricky and time-consuming. A lot depends on the scope of your React app and the complexity of your data. I'll illustrate one of the simplest patterns you can adopt. There are many options and libraries available, such as MobX, Redux, etc.
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 different from useState. The main advantage it has over useState is that covers the use case when there is a need of handling complex data structures or a state object that contains multiple values. It updates the state by accepting a reducer function and an initial state. Then, it returns the actual state and a dispatch function. This dispatch function is used to make changes to the state. As the official documentation of React states.
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.
Create initial data for your store.
We'll create a simple state object without any intricate complexity or nested objects, just for simplicity we'll increment only the count
variable.
const initialState = { count: 0 };
Define your actions identifiers
Define your actions, the keys of your ACTIONS object will be used by the reducer function to identify which update to the state object should be performed.
const ACTIONS = {
INCREMENT: "INCREMENT",
DECREMENT: "DECREMENT"
};
Create the reducer function
The reducer function accepts two arguments the current state and the action to perform on the state object. In this case, for the purpose of the tutorial, we won't send a payload within the action object because we already know what we want to do, just increment 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;
}
};
An example with a payload object sent with the action, you can use the payload to update the new state.
// example of dispatching an action
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;
}
};
Create the Context
Create your context object.
const AppContext = createContext();
Create the store Provider
Now we'll create our StoreProvider
component.
From now, all components will receive a store
object, in the store object you can now access the current state and the dispatch function.
The store object is memoized using the useMemo hook.
const StoreProvider = ({ children }) => {
if (!children) throw new Error("StateProvider! no children given");
const [state, dispatch] = useReducer(reducer, initialState);
const store = useMemo(() => ({ state, dispatch }), [state, dispatch]);
return <AppContext.Provider value={store}>{children}</AppContext.Provider>;
};
Create a simple hook to access your context
Now you need a function that returns your context object. Later you'll use this hook in your component to access the context object that holds your state and the dispatch function.
const useStore = () => {
const context = useContext(AppContext);
if (!context) throw new Error("useStore must be used within StateProvider!");
return context;
};
Export based on your needs.
export { useStore, StoreProvider };
Example
Click the link to show a demo on codesandbox.