
Introduction
At work recently, I was faced with a problem that’s quite common, thanks to React’s one-way data flow. At a high level of my application, I opened a checkout drawer that contained a user’s selection of items to order. In another, much lower component, I triggered the actual ordering of those items. Once the order was successfully placed, I needed the drawer to automatically slide closed again.
Following so far?
Because of the nature of my app’s checkout process, there were a lot of intermediate components between the top-level, parent component (controlling the drawer’s open or closed state), and the final checkout child component inside the drawer (containing the button and function to place the order).
I wanted to avoid having to pass down props containing the drawer’s open state and a function to toggle the drawer’s state back to closed upon order success. None of the other intermediate components needed those props for anything, so it would be extra code that didn’t need to be there. Then I lit upon a solution — something I’d heard a bit about but had never had a need to use: React’s Context.
Today, I’ll be detailing how I used React’s Context API and its useContext hook to avoid prop drilling both application state and functions.
What’s the React Context API?
Before I get too far into the weeds of how I employed the Context API to help solve my problems, let me go over what it is and how it works, so we’re all on the same page.
React’s official documentation does the best job of defining Context:
Context provides a way to pass data through the component tree without having to pass props down manually at every level. — React Docs
Just as in the scenario I described above, Context exists in React as a way to pass data around an application without having to explicitly pass a prop through every level of the tree. It’s React’s built-in way of sharing data that can be considered “global” for a tree of React components.
Examples of this could include user authentication, preferred language or theme setting, or, as with my situation, a child component telling a parent component to switch its state from an open drawer to a closed drawer.
Yes, you could use Redux for this sort of global state management as well, but the boilerplate code required to set up Redux, its actions and its dispatchers, is a lot of extra work for what should be a relatively simple set of tasks. Plus, with the release of React Hooks, using context has been made easier than before.
OK, let’s talk about how to use React’s Context API and the useContext Hook. I’ll show you how to implement it using my own application.
Using React Context
The first thing you need to do to use the Context API is to create a new Context file. This is the file that will hold any data variables, default values, and functions you might want to make accessible to the components when using this context.
React.createContext()
Inside your context file, which I tend to name XyzContext.js, you’ll create this particular instance of Context, and assign it some default values.
Here’s the context file I made for my application to handle the visibility of the checkout drawer and the toggle of its visibility state.
CheckoutDrawerContext.js
import { createContext } from 'react';
const CheckoutDrawerContext = createContext({
showDrawer: false,
toggleCheckoutDrawer: () => {},
});
export default CheckoutDrawerContext;
As you can see from my code snippets, to create a new instance of Context is relatively easy. In my case, after calling creatContext(), I just passed in an object with a boolean about the checkout drawer’s current visibility (showDrawer with a default starting value of false) and an empty arrow function called toggleCheckoutDrawer(), which will eventually be associated with a function that actually affects the drawer’s state of being shown or hidden.
When React renders a component that subscribes to this CheckoutDrawerContext object it will read the current context values from the closest matching Provider above it in the tree.
The default value argument (showDrawer, in this case) is only used when a component does not have a matching Provider above it in the tree.
Now that this particular instance of Context is created, we can move on to using it in my application.
Context.Provider and its values
Every Context object comes with a Provider React component that allows consuming components to subscribe to context changes.
The provider accepts value props to be passed to consuming components that are descendants of this Provider. What’s also good to know is that one Provider can be connected to many consumers. If multiple children need to know or change the values of the provider, they can.
Cart.js
<CheckoutDrawerContext.Provider value={{ showDrawer, toggleCheckoutDrawer }}>
<section className="cart-checkout">
<CartDrawer selectedCartItems={selectedCartItems} />
</section>
</CheckoutDrawerContext.Provider>
For my application, inside of my top level Cart component, I wrapped my CartDrawer component in my CheckoutDrawerContext.Provider. I did this because child components nested within CartDrawer would need access to the values the CheckoutDrawerContext.Provider contains — in this case, the showDrawer state and the toggleCheckoutDrawer() function.
As for the values being passed to the CheckoutDrawerContext.Provider, I made them inside the Cart.js file too.
Cart.js
const [showDrawer, setShowDrawer] = useState(false);
const toggleCheckoutDrawer = () => {
setShowDrawer(!showDrawer);
};
Using the useState() React Hook inside of the Cart component file, I created a piece of state called showDrawer with its setter to update the boolean setShowDrawer. I created a function called toggleCheckoutDrawer(), which would switch the value of showDrawer to whatever its opposite value was.
I named these states and functions after the default values in my CheckoutDrawerContext file, but this is not necessary — I did it only for my own convenience.
Right, so now the Context.Provider is up and running and it has its values of showDrawer and toggleCheckoutDrawer(). I’m ready to move on to the consuming values part.
React.Consumer and React.useContext()
Initially, when Context was first released with React, it was only available to stateful, class-based components.
The syntax for Context consumers is similar to that for Providers to add it to a project — wrap the consumer around the code that needs access to the values, as shown in the sample code below.
ClassBasedChildComponent.js
<SampleContext.Consumer>
{value => /* do something with the context values passed here */}
</SampleContext.Consumer>
But since my application is using React hooks and the checkout flow is written using functional components, I leveraged the useContext() hook for my components that needed access to the CheckoutDrawerContext instead. In my opinion, it’s cleaner code and easier to understand.
CheckoutSummaryDetails.js
import React, { useContext } from 'react';
import { orderItems } from '../../../../services/orderService';
import CheckoutDrawerContext from '../../../../context/CheckoutDrawerContext';
export const CheckoutSummaryDetails = props => {
const { summaryDetails, userInfo, items } = props;
const checkoutDrawer = useContext(CheckoutDrawerContext);
const placeOrder = async () => {
const executionResponse = await orderItems(userInfo, items);
if (executionResponse === 'successfully placed order') {
checkoutDrawer.toggleCheckoutDrawer();
}
};
return (
<div className="cart-drawer__summary">
<strong className="cart-drawer__header">Summary</strong>
<div className="cart-drawer__summary-content">
<div className="checkout-summary">
<span>Final Checkout Summary</span> <span>{summaryDetails}</span>
</div>
</div>
<button type="button" className="order-button" onClick={placeOrder}>
Order Now
</button>
</div>
);
};
The component above, CheckoutSummaryDetails.js, is a very stripped-down version of the actual checkout in my application, but it serves its purpose for this example.
First, note that the CheckoutDrawerContext is imported into this CheckoutSummaryDetails.js file. To actually gain access to the values of this Context, I create a new constant (checkoutDrawer), and wrap the named Context in the useContext() Hook. This gives me access to values within the CheckoutDrawerContext. The rest is fairly straightforward.
When the user clicks the button titled “Order Now,” the placeOrder() function is fired in the component and it triggers the orderItems() call to an API or a database or wherever else it might need to send the user info and the items being ordered. Upon a success message of the order being placed, the component that closes the CartDrawer is triggered via checkoutDrawer.toggleCheckoutDrawer(). This will switch the state of showDrawer to some four or five levels higher up this component tree, from true back to false, and the CartDrawer component will be hidden from view again.
That’s it! React’s Context has prevented unnecessary prop passing and allowed a child component to update the state of a parent component. Sweet!
That wasn’t so hard now, was it? Believe me, once you’ve used Context successfully once, implementing it in lots of other places will be a cinch.
Conclusion
React’s Context API and useContext Hook are fairly easy to understand in theory. They’re a little less clear when it comes to implementing them the first time around.
But they’re such a help. Passing unneeded data through several levels of a component tree when only the second and sixth level of child components actually need those props is not helpful. It just means more props to keep track of, even when all the intermediate components don’t need that data.
Yes, Redux could do the same thing, but that’s a lot of extra setup for even the simplest pieces of data. And unlike Redux, Context is native to React — it’s already built-in, it’s ready to go, and once you start using it, it’s pretty intuitive. I was hesitant to try it at first, but once I figured it out, I became a big fan. I think you will be too.
Check back in a few weeks — I’ll be writing more about JavaScript, React, ES6, or something else related to web development.
Thanks for reading. I hope I’ve demystified React’s Context API and useContext Hook so you can successfully use it in your own React project. You won’t regret the cleaner code that it makes possible!
References and Further Resources
- React Context API documentation
- React Hooks useContext documentation