Context API + useReducer in React
Lately I have been playing around with React’s Context API and exploring its use cases. Someone recently shared a very insightful blog article with me that takes a deep dive into why Context API is not a state management tool. I recommend reading the entire article but if you are limited on time then cut to the recommendations section to understand when to use Context vs. Context + useReducer vs. Redux
in your application. See article:
The important thing to know is Context in and of itself is NOT a state management tool, while Context + useReducer IS but with limitations compared to Redux. When you use useReducer
with Context , a new state value is created when updates are made to state. This leads to the re-rendering of all components that are subscribed to the said context, which is not ideal from a performance and efficiency standpoint. Another limitation is you don’t have access to Redux DevTools which is very useful when tracking actions and state over time.
In this short walkthrough we will integrate Context API and useReducer as a way to manage global state throughout your application.
This application is an expense tracker that will utilize a single context. The single context is going to track our monetary transactions. We are going to manage our state with Context + useReducer and set it up as a global management system, so that we have access to the transactions everywhere. We will create a directory under /src
named /contexts
where we will have our TransactionContext.js
and TransactionReducer.js
files. You could also replace the word “Transaction” with “Global” as this is our global state, but it’s good to use a semantic naming convention, especially if our application grows and we end up separating the global context into individual contexts.
Ultimately we want our entire application to have access to all the data inside TransactionContext.js
. So inside App.js
we need to wrap everything we return from this component with the context provider. See Lines 7, 12, and 18 below:
Now on to the TransactionContext.js
file. Here we need access to createContext and useReducer so let’s import that along with our reducer which has yet to be created:
import React, { createContext, useReducer } from ‘react’;
import TransactionReducer from ‘./TransactionReducer’
Then we need to create the context. You can create a context without an initial value, but we know we want the state (store) to hold all of our transactions so we can pass in an object with a key of transactions and an empty array as the value:
const initialState = {
transactions: []
}
export const TransactionContext = createContext(initialState);
Then, we want to build our TransactionContextProvider
function. This function is where we integrate useReducer
and return the children prop wrapped in the provider with all of the state we want our application to have access to as the value prop. Yes, that sounds confusing but this is all that it means:
Since we wrapped everything in App.js
with our Provider, we have access to children
inside TransactionContextProvider
above. So children
will have access to whatever we pass in as the value. Above, we are passing in an object with transactions as the key and state.transactions
as the value. State
comes from useReducer
which takes two arguments (our reducer and the initial state).
We also added an action named addTransaction
. When invoked, this function will dispatch the type
and payload
to our reducer, which then updates our application’s state accordingly. We pass addTransaction
as part of our provider value
prop so that our application has access to this action.
So what does our reducer look like? Well, if you’re familiar with Redux it’s essentially then same setup. We utilize a switch statement with multiple cases. In this case, when the ADD_TRANSACTION
action is dispatched, it will run the case for it and return the new state with updates.
The setup is finished! Now, in order to retrieve something from our application’s state, we just need to utilize the useContext
hook. So in this example, we have a component named CreateTransaction.js
that will execute the ADD_TRANSACTION
action when we submit a form. Something like this:
On line 6 we destructure the addTransaction
function from TransactionContext
. Then on line 18 we invoke the function with input from the user. This goes to our reducer and updates our global state. We can now implement useContext
in any of our components and have access to our entire state!
You can continue building upon the TransactionContext
.js file by adding additional actions and passing in more values to the provider. You can make your actions asynchronous as you will most likely have a backend at some point. See below:
Just update your reducer to handle the additional cases added from the new types above and you’ll be good to go!
That’s it!