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} />
}
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",
})
... 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:
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")
)
That gives you a Context object which is properly typed...
... and if you try to use the context object without initializing it, you get a helpful error:
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
}
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)