Instead of using React.createContext directly, we can use a utility function to ensure the component calling useContext is rendered within the correct Context Provider.
// JavaScript:
const [BananaProvider, useBanana] = createStrictContext()
// TypeScript:
const [BananaProvider, useBanana] = createStrictContext<Banana>()
Scroll down for the code, or find it in this gist.
The Problem
We would normally create a React Context like this:
const BananaContext = React.createContext()
// ... later ...
const banana = React.useContext(BananaContext) // banana may be undefined
Our banana will be undefined if our component doesn't have a BananaContext.Provider up in the tree.
This has some drawbacks:
- Our component needs to check for undefined, or risk a run-time error at some point.
- If bananais some data we need to render, we now need to render something else when it'sundefined.
- Basically, we cannot consider our bananaan invariant within our component.
Adding a custom hook
I learned this from a blog post by Kent C. Dodds.
We can create a custom useBanana hook that asserts that the context is not undefined:
export function useBanana() {
  const context = React.useContext(BananaContext)
  if(context === undefined) {
    throw new Error('The useBanana hook must be used within a BananaContext.Provider')
  return context
}
If we use this, and never directly consume the BananaContext with useContext(BananaContext), we can ensure banana isn't undefined, because it if was, we would throw with the error message above.
We can make this even "safer" by never exporting the BananaContext. Exporting only its provider, like this:
export const BananaProvider = BananaContext.Provider
A generic solution
I used the previous approach for several months; writing a custom hook for each context in my app.
Until one day, I was looking through the source code of Chakra UI, and they have a utility function that is much better.
This is my version of it:
import React from 'react'
export function createStrictContext(options = {}) {
  const Context = React.createContext(undefined)
  Context.displayName = options.name // for DevTools
  function useContext() {
    const context = React.useContext(Context)
    if (context === undefined) {
      throw new Error(
        options.errorMessage || `${name || ''} Context Provider is missing`
      )
    }
    return context
  }
  return [Context.Provider, useContext]
}
This function returns a tuple with a provider and a custom hook. It's impossible to leak the Context, and therefore impossible to consume it directly, skipping the assertion.
We use it like this:
const [BananaProvider, useBanana] = createStrictContext()
Here's the TypeScript version:
import React from 'react'
export function createStrictContext<T>(
  options: {
    errorMessage?: string
    name?: string
  } = {}
) {
  const Context = React.createContext<T | undefined>(undefined)
  Context.displayName = options.name // for DevTools
  function useContext() {
    const context = React.useContext(Context)
    if (context === undefined) {
      throw new Error(
        options.errorMessage || `${name || ''} Context Provider is missing`
      )
    }
    return context
  }
  return [Context.Provider, useContext] as [React.Provider<T>, () => T]
}
We use it like this:
const [BananaProvider, useBanana] = createStrictContext<Banana>()
Conclusion
We can make errors appear earlier (unfortunately still at runtime) when we render a component outside the required Context Provider by using a custom hook that throws when the context is undefined.
Instead of using React.createContext directly, we use a utility function to create providers and hooks automatically for all the contexts in our app.
Comments?
- Do you use a similar "pattern"? No? Why not?
- In which cases would you NOT use something like this?
References:
- How to use React Context effectively by Kent C. Dodds
- Original utility function in the Chakra UI repo.
- Gist with both JS and TS versions of the function
- React Context Documentation
Photo by Engjell Gjepali on Unsplash
 
 
              
 
    
Top comments (2)
I also use KCD's context pattern (mentioned in the linked blog).
Instead of exposing
dispatchas KCD did, I sometimes exposeactions(functions that wraps dispatch calls) down the context.Good article, look at this one!
use-one.com