The best way to initialize a React Context in TypeScript
Tags: react, typescript
Published: 2023-06-14 01:13:46

[React Contexts](https://react.dev/reference/react/useContext) 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](https://gist.github.com/erikpukinskis/ffc080bbd087df7ee4567421c186ae13) 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:

```tsx
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} />
}
```

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

```tsx
import { createContext } from "react"

const DesignSystemContext = createContext({
  theme: "light",
})
```
... 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'.
Property "theme' is missing in type '{}' but required in type 'DesignSystemContextValue'. ts (2345)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nq6cnoc4vchqecxspow9.png)

## 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](https://gist.github.com/erikpukinskis/ffc080bbd087df7ee4567421c186ae13).

The `makeUninitializedContext` function returns a [Proxy object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) that is typed with whatever type you want:

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

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

const DesignSystemContext = createContext(
  makeUninitializedContext<DesignSystemContextValue>("Cannot use DesignSystemContext outside a DesignSystemProvider")
)
```

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1xh3p1pxg92jr3p9s5xj.png)

... 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n9jysx94kjq2ry6590c0.png)

### 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](https://gist.github.com/erikpukinskis/ffc080bbd087df7ee4567421c186ae13). It allows you to detect whether a context is initialized before you try to use it:

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

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

  return isInitialized(context) ? context.theme : undefined
}
```

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](https://twitter.com/erikpuk) for more React tips.