Typing React Context In TypeScript

Updated on · 5 min read
Typing React Context In TypeScript

When working on a TypeScript project in React, it is important to ensure type safety and proper typing for all components and context. TypeScript provides static type checking that helps catch errors and provides better editor support, making it easier to maintain and refactor code. React context is a powerful feature that allows us to share data and state across components in a tree-like structure. However, when it comes to typing context in TypeScript, there are some considerations and challenges to overcome.

In this article, we will explore how to type React context in TypeScript specifically. We'll focus on creating a theme context that supports both dark and light themes, and we'll address various type-related issues that arise during the process. We'll cover concepts such as creating and providing context, defining context types, and handling cases where the context value is initially undefined. By following the steps and best practices outlined in this article, you will have a clear understanding of how to effectively type your React context using TypeScript, enabling you to build more robust and type-safe applications.

Getting started

Let's imagine we're working with an application that supports both dark and light themes. Our goal is to introduce context, enabling us to store the selected theme and share it across various components.

js
// context.js import { createContext, useContext, useState } from "react"; export const ThemeContext = createContext(); export const ThemeProvider = ({ children }) => { const [theme, setTheme] = useState("dark"); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ); }; export const useThemeContext = () => { const context = useContext(ThemeContext); if (!context) { throw new Error("useThemeContext must be used inside the ThemeProvider"); } return context; };
js
// context.js import { createContext, useContext, useState } from "react"; export const ThemeContext = createContext(); export const ThemeProvider = ({ children }) => { const [theme, setTheme] = useState("dark"); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ); }; export const useThemeContext = () => { const context = useContext(ThemeContext); if (!context) { throw new Error("useThemeContext must be used inside the ThemeProvider"); } return context; };

We begin by creating ThemeContext. This will hold the current theme value and a setter function for it. To simplify usage across the application, we introduce a utility hook, useThemeContext. This eliminates the need for explicit ThemeContext imports in other parts of the app. Additionally, the hook ensures that it's only called from components wrapped within the ThemeProvider.

This context is then used within the ThemeSwitch component, which is responsible for toggling between themes.

js
// components/ThemeSwitch.js import { useThemeContext } from "./context"; export const ThemeSwitch = () => { const { theme, setTheme } = useThemeContext(); return ( <div> <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}> Toggle theme </button> </div> ); };
js
// components/ThemeSwitch.js import { useThemeContext } from "./context"; export const ThemeSwitch = () => { const { theme, setTheme } = useThemeContext(); return ( <div> <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}> Toggle theme </button> </div> ); };

Adding types to the context

Now, let's proceed with adding types to the context. The first issue we encounter after renaming context.js to context.tsx comes from the createContext function:

markdown
Expected 1 arguments, but got 0. index.d.ts(377, 9): An argument for 'defaultValue' was not provided.
markdown
Expected 1 arguments, but got 0. index.d.ts(377, 9): An argument for 'defaultValue' was not provided.

Upon examining the type definition for createContext, we find that it expects a non-optional argument for the default state. This topic has been extensively discussed on GitHub. One comment provides a rationale for this requirement. In essence, we must pass a default value, which will also serve to infer the type for the context.

tsx
import { createContext } from "react"; export const ThemeContext = createContext({ theme: "dark", setTheme: (theme: string) => {}, });
tsx
import { createContext } from "react"; export const ThemeContext = createContext({ theme: "dark", setTheme: (theme: string) => {}, });

If you're curious about other aspects of using TypeScript in React components, you might find these articles interesting: TypeScript: Typing React UseRef Hook and TypeScript: Typing Form Events In React.

This addresses the type inference issue. All we have left to do is to define the types for the children prop within the ThemeProvider.

tsx
import { PropsWithChildren, useState } from "react"; export const ThemeProvider = ({ children }: PropsWithChildren<{}>) => { const [theme, setTheme] = useState("dark"); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ); };
tsx
import { PropsWithChildren, useState } from "react"; export const ThemeProvider = ({ children }: PropsWithChildren<{}>) => { const [theme, setTheme] = useState("dark"); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ); };

With these changes, both theme and setTheme will now have the correct types when used within the ThemeSwitch.

Handling cases where context is initially undefined

The ideal scenario described above isn't always achievable. There may be instances where the default context value depends on other variables, or simply cannot be determined at the time of context creation. In such situations, we need to explicitly assign undefined as the default value to createContext. Additionally, we need to define the context type and pass it to createContext to ensure the correct typing of the values.

tsx
import { createContext } from "react"; type ContextType = { theme: string; setTheme: (theme: string) => void; }; export const ThemeContext = createContext<ContextType | undefined>(undefined);
tsx
import { createContext } from "react"; type ContextType = { theme: string; setTheme: (theme: string) => void; }; export const ThemeContext = createContext<ContextType | undefined>(undefined);

In this code snippet, we're specifying that the context value can either be an object encompassing all properties present in ContextType, or it can be undefined.

However, this won't resolve all type-related issues, especially if we lack a default value within the provider, in which case the theme's type will be inferred as undefined.

tsx
import { PropsWithChildren, useState } from "react"; export const ThemeProvider = ({ children }: PropsWithChildren<{}>) => { const [theme, setTheme] = useState(); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ); };
tsx
import { PropsWithChildren, useState } from "react"; export const ThemeProvider = ({ children }: PropsWithChildren<{}>) => { const [theme, setTheme] = useState(); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ); };

We could try to fix it by specifying the type of state explicitly:

tsx
const [theme, setTheme] = useState<ContextType["theme"]>();
tsx
const [theme, setTheme] = useState<ContextType["theme"]>();

In this instance, we're creating a state with a type of ContextType['theme']. Since we haven't provided a default value, it's implicitly set to undefined. This is automatically incorporated into the state's type, making the state's type ContextType['theme'] | undefined.

Despite this, we still encounter a type error in the value for ThemeContext.Provider: value={{ theme, setTheme }}.

markdown
TS2322: Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'.
markdown
TS2322: Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'.

Currently, the state is ContextType['theme'] | undefined, but the context type expects a ContextType['theme'] for that property. To fix this, the simplest solution is to explicitly pass a default value of the same type as the context value. In our case, this would be an empty string.

tsx
const [theme, setTheme] = useState<ContextType["theme"]>("");
tsx
const [theme, setTheme] = useState<ContextType["theme"]>("");

This ensures that the theme type is consistently a string, never undefined. With this adjustment, our context is now correctly typed.

tsx
import { PropsWithChildren, createContext, useContext, useState } from "react"; type ContextType = { theme: string; setTheme: (theme: string) => void; }; export const ThemeContext = createContext<ContextType | undefined>(undefined); export const ThemeProvider = ({ children }: PropsWithChildren<{}>) => { const [theme, setTheme] = useState<ContextType["theme"]>(""); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ); }; export const useThemeContext = () => { const context = useContext(ThemeContext); if (!context) { throw new Error("useThemeContext must be used inside the ThemeProvider"); } return context; };
tsx
import { PropsWithChildren, createContext, useContext, useState } from "react"; type ContextType = { theme: string; setTheme: (theme: string) => void; }; export const ThemeContext = createContext<ContextType | undefined>(undefined); export const ThemeProvider = ({ children }: PropsWithChildren<{}>) => { const [theme, setTheme] = useState<ContextType["theme"]>(""); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ); }; export const useThemeContext = () => { const context = useContext(ThemeContext); if (!context) { throw new Error("useThemeContext must be used inside the ThemeProvider"); } return context; };

If you take a closer look at the code above, you'll see that even though we've given our theme a default state value, its type can still be undefined according to ThemeContext. So, why aren't we getting any type errors when using the context?

That's where our useThemeContext helper comes in. It checks to make sure the context is defined before it gets returned, so that takes care of any undefined issues. It also throws an error if it's not defined, which works for us because we always give the context a value when it's used inside the provider. But if that wasn't the case, like if the context only had a string value that started as undefined, we'd need to handle that differently and not throw an error if undefined is a possible context value.

Context types as an array

What if we want the context to look just like the result we get from useState? We can tweak our ContextType to be an array that includes the state and the state setter, instead of an object. This lets us skip the step of breaking down the useState values and pass them straight to the context provider.

tsx
import { PropsWithChildren, createContext, useState } from "react"; type ContextType = [string, (theme: string) => void]; export const ThemeContext = createContext<ContextType | undefined>(undefined); export const ThemeProvider = ({ children }: PropsWithChildren<{}>) => { const value = useState(""); return ( <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider> ); };
tsx
import { PropsWithChildren, createContext, useState } from "react"; type ContextType = [string, (theme: string) => void]; export const ThemeContext = createContext<ContextType | undefined>(undefined); export const ThemeProvider = ({ children }: PropsWithChildren<{}>) => { const value = useState(""); return ( <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider> ); };

With these tweaks in place, we'll need to adjust the useContext call inside the ThemeSwitch. After that, all the types should work correctly.

tsx
// components/ThemeSwitch.tsx import { useThemeContext } from "./context"; export const ThemeSwitch = () => { const [theme, setTheme] = useThemeContext(); return ( <div> <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}> Toggle theme </button> </div> ); };
tsx
// components/ThemeSwitch.tsx import { useThemeContext } from "./context"; export const ThemeSwitch = () => { const [theme, setTheme] = useThemeContext(); return ( <div> <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}> Toggle theme </button> </div> ); };

Conclusion

In this article, we dived into the topic of typing React context in TypeScript, a crucial aspect of developing type-safe and reliable React applications. We focused on creating a theme context that supports dark and light themes while addressing various type-related challenges along the way.

We explored the process of creating and providing context in React using TypeScript, ensuring that the data and state shared across components have proper typing. We discussed how to define context types, handle cases where the context value is initially undefined, and provided insights into alternative approaches like using context types as arrays.

References and resources