Using Context to share state across components
State management can seem like an overwhelming task for React developers. In the past, maintaining a state that was shared in different parts of our component tree was extremely complicated, and we typically resorted to third-party state management libraries to help us out. But it’s time for us to update our thinking!
State management in React is easy now that we have hooks and the context API. Let’s look at how we can manage a piece of state throughout our app using nothing but the tools we would already be using and those that come built-in with React!
When faced with the challenge of managing state in our React apps, some devs may be quick to look to state management libraries like Redux to solve all of our problems. But if we think carefully about what we need to do, we may realize that the pieces of state that we’re considering don’t need to be global at all.
For example, imagine that we’re building a notification component. This component needs to be aware of reminders and tasks that come from our backend, and make them available in a side drawer for the user to read and mark as viewed.
Option 1 — Complicate Things
If we’re in a Redux state of mind, we may be quick to assume we need to implement a pretty complicated thunk action that makes a GET request and dispatches an action to our reducer which adds the notifications to our data store, which then is accessed by mapping state to props in our notifications drawer component.
When the user marks notifications as read another dispatch thunk action is fired, which send a POST request to the backend and on success another action is dispatched that removes that specific item from our data store…that’s way too complicated!!
Option 2 — Make Your Life Easy
When we think carefully and creatively about this problem, we may realize that there is no need for this to be a piece of global state at all. The notifications component is really the only one that needs to be aware of this data.
In fact, it probably doesn’t need to be stored in the state at all! What if we used a simple, custom hook that was responsible for fetching and returning the notifications, and for sending the
markAsRead request? All that complicated state management spaghetti code could probably be cleaned up, abstracted out of the component, and used with just a single line in the component!
(For a more thorough explanation of how to use this technique, check out Asynchronous State Management With React-Query)
By adjusting our thinking, we end up with a very clean component
<NotificationsDrawer /> and a custom hook that’s much more readable than an implementation of a full-blown state management system. It will also be much easier to maintain in case we need to make a change in how/what we fetch down the road.
That being said, there will be times that we need to have states shared across very different parts of our component tree, and the above technique won’t work. This is especially true when we want the user to have control over what data is selected/used, as opposed to just needing to access the response from our backend. This is where the Context API shines!
React’s Context API provides a way for us to access state deep in the component tree without needing to pass props to every single component along the way (known as “prop drilling”).
This API will cover almost any case where we need to manage a simple state that’s shared throughout our app. Implementing this technique will require us to build two related pieces — a provider and a hook for accessing the state from the provider.
For this example, let’s pretend we have a baseball statistics app. Users can select a team in the top menu bar, and every page on the app now uses data specific to that team.
For example, a user is on a page that shows the current roster and selects the Boston Red Sox from the top menu bar. They now see the current roster of the Red Sox. When they navigate to a page that shows the current batting statistics we want them to see those stats for the Red Sox, instead of some default team, which would force the user to have to reselect their team on each page (not very good UX! ).
Without the Context API, this simple requirement would be very complicated to implement. We would either need to be passing the team through props all over the component tree (“prop drilling”) or we’d have to throw in the towel and use a heavy third-part state management library (yuck!).
Here’s how our implementation of this feature might look if we use context…
This handles everything! What exactly is stored in the context? We have
allTeams, which is an array of all the team objects returned from the backend. This can be used to create the options for a select component that we’ll put in the top menu bar for a user to change the selected team.
We also have
team which we will use as the team that is currently selected by the userand
setTeam which we can call when a user changes his selection (this is what will be called in the
onChange of the team select component).
Using the hook
Now, what do we do with this? All of these pieces of data stored in the context are available through use of the
useTeamContext hook we export at the bottom of the file. How would we actually use that hook? Here’s an example select component that allows a user to select the team that will be used throughout the app…
No dispatching, no selectors, nothing! Just a clean, simple hook that gives us both the data we need, and the function for changing the selection across the app.
While the hook we built is what we’ll be using most often, the Provider is just as important!
What About the Provider?
We can only use our
useTeamContext hook when we are in a descendant component of the
Provider. This is while our
<TeamsProvider /> component took
children as a prop!
If we expect to use our context hook all over our app, we should put the
TeamsProvider component really high in the component tree. In fact, it’s very common to see providers in the
<App /> component, since that effectively means your whole application will have access to the same context it provides.
In many cases, we can escape the traps and complexities of third-party state management libraries by just adjusting our thinking, and really evaluating what needs access to stateful data/ how it can access it.
Many times we can implement a pretty simple hook to meet our needs. Sometimes we may actually need some way of shared state among components that live in very different parts of the component tree.
In those cases, React’s built-in Context API is super helpful! Becoming familiar with this API can help you write clean, maintainable, readable code. Your team and your future self will thank you!