DEV Community

Cover image for Why I never use React.useContext
Julian Garamendy
Julian Garamendy

Posted on • Updated on • Originally published at juliangaramendy.dev

Why I never use React.useContext

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>()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 banana is some data we need to render, we now need to render something else when it's undefined.
  • Basically, we cannot consider our banana an 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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

We use it like this:

const [BananaProvider, useBanana] = createStrictContext<Banana>()
Enter fullscreen mode Exit fullscreen mode

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:


Photo by Engjell Gjepali on Unsplash

Latest comments (2)

Collapse
 
suhaotian profile image
suhaotian

Good article, look at this one!

use-one.com

Collapse
 
dance2die profile image
Sung M. Kim

I also use KCD's context pattern (mentioned in the linked blog).

Instead of exposing dispatch as KCD did, I sometimes expose actions (functions that wraps dispatch calls) down the context.