React has always had its strengths and weaknesses. Back in the good olโ days, when mixins werenโt considered harmful, one of the biggest grievances was that you had to pass shared state from the very top of your app, where you stored the important stuff, to the leaf components that rendered the data.
To solve this, there was something called the unofficial, do-not-use, very unofficial Context API, and some libraries, like Redux, used this feature to great success. Even though it was documented, it came with a big, fat warning upfront that you probably shouldnโt. The original API was clunky and hard to use, and it was never meant as a user-facing feature. It was added because of necessity, but it was always scheduled for deprecation.
Today, we have better tools at our disposal. The new Context API was released in 16.3.0, and came with the concept of a context provider and consumer. With the addition of Hooks in 16.8, we could even consume the context via a Hook, improving developer ergonomics even further.
This article is going to dive into what a context is, when you should be reaching for them, and the best way to create them.
Iโve always described contexts as Reactโs wormhole. You pass some value into it, nest some component tree inside of it, and then you can retrieve those values without having to pass them as props. Itโs magic, really.
Another way to think of contexts is that they work as a โshortcutโ for your code. They can help you create smoother APIs, and disconnect component structures from your data flow.
However, when you think of contexts, they are a very useful feature in React. A feature that isnโt used often enough, in my opinion.
The Context API is a great tool to use when you donโt want to pass a value directly by props. There are several use-cases for that.
Context is a great tool for those situations where you have a piece of state that needs to be shared across your application. The current language or color scheme, for instance, are great examples of this kind of state.
You can also use contexts at a lower level, for any given part of your application, so donโt feel you need to have all those Providers in your index.tsx
file.
Sometimes, you create components that go together. You need to share some information between them, but you donโt want to pass the same prop for all of them.
My favorite example of this is form controls and IDs. The label needs to know the ID of the input field it describes, and any error messages or further description elements also require it for setting the correct aria-tags. Passing it manually is an error-prone process, and just passing it into a top-level component would be much more preferable.
Okay, so weโve covered what a context is and when itโs a good tool to have. But whatโs the best way to structure them?
I came across this wonderful article by Kent C. Dodds some time ago, and this approach is heavily influenced by that article. Itโs definitely recommended reading.
This technique creates a single file that only exposes two things โ a context provider and a consumer Hook. It doesnโt expose the underlying context object, and it has no fallback value in case you forgot to do something.
Hereโs an example, where we create a language switcher:
const LanguageContext = React.createContext(); export const LanguageProvider = ({ defaultLanguage = 'en', children }) => { const [language, setLanguage] = React.useState(props.defaultLanguage); const contextValue = React.useMemo( () => ({ language, setLanguage }), [language] ); return ( <LanguageContext.Provider value={contextValue}> {props.children} </LanguageContext.Provider> ); } export const useLanguage = () => { const context = React.useContext(LanguageContext); if (!context) { throw new Error("You have to wrap this component in a LanguageProvider"); } return context; }
That was a lot of code โ so letโs go through it step by step:
const LanguageContext = React.createContext();
This is where we create the context itself. Note that we donโt pass a default value to the context โ thatโs an intentional choice. Although there are cases where a default value would make sense, I typically avoid it. This way, I can warn the developer that they have forgotten to wrap some part of the app in the provider:
export const LanguageProvider = ({ defaultLanguage = 'en', children }) => { const [language, setLanguage] = React.useState(defaultLanguage); const contextValue = React.useMemo( () => ({ language, setLanguage }), [language] ); return ( <LanguageContext.Provider value={contextValue}> {props.children} </LanguageContext.Provider> ); }
This is the Provider-component. Its responsibility is to contain the state weโre passing down and then passing that value into the context we created in the last step.
You might notice that weโre wrapping the context value in a call to the React.useMemo
Hook. This is done to avoid unnecessary re-renders. If we skipped it โ all consumers of this context would re-render whenever this provider re-rendered. And even though you shouldnโt pre-optimize too much, wrapping contexts in useMemo
like above is usually a safe bet.
Note that you could go a bit overboard with optimizing for as few re-renders as well. You could โ in theory โ create two different contexts, one for the value, and one for the update-function so that components that only deal with changing the value instead of displaying it would avoid re-renders. Itโs not an approach Iโve ever found an actual use case for, but itโs a cool trick to know about. Just donโt do it because it said so on the internet.
export const useLanguage = () => { const context = React.useContext(LanguageContext); if (!context) { throw new Error("You have to wrap this component in a LanguageProvider"); } return context; }
The last part of the puzzle is the consumer Hook. Here, we use the React.useContext
Hook to retrieve the context value. We add a check to make sure the developer using this Hook has remembered to wrap it in a provider component (itโs an easy mistake!), and then just return the context.
I like to pass down the โrawโ values and updater functions via context, and create better abstractions in the Hook. This way, I can create new Hooks based on the same API, and I let the final API be up to the Hook, not the provider. There wasnโt much of a need for that in this example though!
With these parts, youโll have all you need to create really nice contexts with a very terse, concise API. All you expose are two items โ a context provider and a hook to get that value back. Thatโs pretty neat!
I hope this article gave you a few new techniques for dealing with contexts. Thanks for reading!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
The AHA stack โ Astro, htmx, and Alpine โ is a solid web development stack for smaller apps that emphasize frontend speed and SEO.
Explore what Hattip is, how it works, its benefits and key features, and the differences between Hattip and Express.js.
React Shepherd stands out as a site tour library due to its elegant UI and out-of-the-box, easy-to-use React Context implementation.
Cookies are crucial to web development. This article will explore how to handle cookies in your Next.js applications.