How we handle React Context
In React, the UI is composed by components: independent, reusable pieces that communicate with other components via props.
Conceptually, components are like JavaScript functions. They accept arbitrary inputs (called “props”) and return React elements describing what should appear on the screen.
It’s an elegant design but makes things cumbersome when dealing with cross application concepts like i18n or authentication, where you need to manually pass props down through deep component trees. To fix this, the React Team created the Context API.
React Context started as an experimental API tied to prop-types and class components, and later evolved into its definitive Provider/Consumer approach on the 16.3 release . It’s fair to say that it’s another piece of elegant design and fits nicely into React’s component based architecture.
Then came React 16.8 and Hooks.
Hooks let you use state and other React features without writing a class. You can also build your own Hooks to share reusable stateful logic between components.
Hooks support React Context via the useContext
Hook.
At Trabe, we have been using React Context since our first projects. We also have embraced Hooks. We think that’s the best way to write our components. Let’s see how we use React Context nowadays combined with Hooks.
A simple example: an auth provider
We need to share across several components some auth info like the current logged user. Other components may need to login or logout an user. We use the Context to pass the user info and some operations down the component hierarchy. Components will grab both user and operations using a custom Hook.
With the provider and the Hook in place we can use it in our app with ease.
Notice what we did:
- We passed an API, not just a value. Child components may need to affect the state of our provider component.
- The component handles its own state. The API methods call
setState
as needed. - Child components use a custom Hook to grab the API. The use of React Context does not leak to the children. Our custom Hook just delegates to
useContext
, but it could have added some sort of wrapper or filtered the context value if needed. - We rely on
useMemo
to memoize our auth API. It will only change when theuser
stored in the state changes. If we don’t memoize the API, asetState
in the provider will create a newContextvalue
, forcing all consumers to re-render even if thatvalue
is the same (theuser
has not changed). You can probably skip this memoization in most cases, but it won’t hurt. There’s a lot of room to talk about how and when to useuseMemo
anduseCallback
, but that’s out of the scope of this post. - We set the context default value to a “null” API. Remember that this is not the initial value stored in the context. It’s a default value that consumers will see if they aren’t nested under a provider.
- We do not rely on the default context value for tests. We pass all provider
props
to theContext.Provider
, effectively allowing a test to overwrite the Context value passing avalue
prop.
To support the static contextType API in classes we could export the Context, but that’s leaky and couples our code to the React Context. For class components we pass everything a component needs as plain props, and wrap the class component with a custom consumer (with a render prop) or a Higher Order Component.
These approach is how we also handle legacy components. When using the old experimental Context API we tried to hide as much as possible the use of context. We relied on these custom consumers or HOCs to avoid coupling. Transition to the new React Context API was painless: just switching to the new consumer or HOC version.
Summing up
The React Context API is great. It solves with elegance the problem of passing stuff deep down your component hierarchy. However, it’s a good practice to avoid coupling your components with the Context API. It’s easy to do so by making your components receive what they need via props
or using custom Hooks.