DEV Community

Cover image for The best way to initialize a React Context in TypeScript
Erik Pukinskis
Erik Pukinskis

Posted on

The best way to initialize a React Context in TypeScript

React Contexts are great—they let you neatly encapsulate behavior that cuts across multiple React components.

But there are a few gotchas when using them in TypeScript.

This article will explain how to deal with one.

TL;DR

You can't easily initialize a React Context if it has required properties. Because you won't know what those properties are when you call createContext.

My makeUninitializedContext gist solves that.

The Problem

A common pattern when using React Contexts is to pass some props into your Context Provider. For example, if your app has dark mode and a light mode, your design system provider might have a theme prop:

import { DesignSystemProvider } from "~/design-system"
import { useState, useEffect, createContext } from "react"

type DesignSystemContextValue = {
  theme: "light" | "dark"
}

const DesignSystemContext = createContext(
  /* what goes here??? */
)

function App() {
  const [theme, setTheme] = useState<"dark" | "light" | undefined>()

  useEffect(() => {
    const listener = (event: MediaQueryListEvent) => {
      setTheme(event.matches ? "dark" : "light")
    }

    const query = window.matchMedia("(prefers-color-scheme: dark)")

    query.addEventListener("change", listener)

    return () => query.removeEventListener("change", listener)
  }, [])

  if (!theme) return null

  return <DesignSystemProvider theme={theme} />
}
Enter fullscreen mode Exit fullscreen mode

The trouble is, how do you then initialize your context? It's easy if you can use a default value...

import { createContext } from "react"

const DesignSystemContext = createContext({
  theme: "light",
})
Enter fullscreen mode Exit fullscreen mode

... but ...

What if you don't want a default value?

What if you want to test that your hooks behave a certain way when used outside the Context Provider?

What if your Context Provider depends on data objects that have no sane default value, like a user or a workspace?

If you try to create a context with an uninitialized object, TypeScript will yell at you:

Source code showing type error: Argument of type '{}' is not assignable to parameter of type 'DesignSystemContextValue'.<br>
Property "theme' is missing in type '{}' but required in type 'DesignSystemContextValue'. ts (2345)

The Solution

The way I solve this is by including a small snippet of code in almost every React project I work on, called makeUninitializedContext.

The makeUninitializedContext function returns a Proxy object that is typed with whatever type you want:

import { createContext } from "react"
import { makeUninitializedContext } from "~/helpers"

type DesignSystemContextValue = {
  theme: "dark" | "light"
}

const DesignSystemContext = createContext(
  makeUninitializedContext<DesignSystemContextValue>("Cannot use DesignSystemContext outside a DesignSystemProvider")
)
Enter fullscreen mode Exit fullscreen mode

That gives you a Context object which is properly typed...

Image description

... and if you try to use the context object without initializing it, you get a helpful error:

Screenshot of uncaught error: Cannot use DesignSystemContext outside a DesignSystemProvider: tried getting context.theme

Making the context optional

Sometimes you have a hook that can work in either of two ways: with or without the context.

For that case, I use the isInitialized function exported in that same gist. It allows you to detect whether a context is initialized before you try to use it:

import { useContext } from "react"
import { isInitialized } from "~/helpers"

export function useThemeName() {
  const context = useContext(DesignSystemContext)

  return isInitialized(context) ? context.theme : undefined
}
Enter fullscreen mode Exit fullscreen mode

That way your hook can work either inside or outside of the Context Provider. And you won't trigger an error by accessing a property on the uninitialized context object.

I hope that's helpful for someone! Follow me on Twitter for more React tips.

Top comments (0)