How to use React Context with TypeScript

The React Context API allows us to make data globally available. We could also do this by passing down props, but this isn't very efficient for deeply nested React components.

There are two things needed for creating a new Context: A provider and a consumer.

You can think of the provider as the store that contains the data. A consumer is a component that uses the data of the provider.

In this tutorial, you will learn how to use React Context with TypeScript, including function and class components.

I created a small example codepen for this tutorial here.

TLDR - How to make React Context TypeScript compatible

If you already know how to use React Context, here's the quick summary on how to use it with TypeScript:

  • Create a Context and Provider just like you would in JavaScript.

  • If you don't use default values, you need to provide an interface:

    interface IThemeContext {
      dark: boolean;
      toggleDark?: () => void;
    }
    
    const defaultState = {
      dark: false,
    };
    
    const ThemeContext = React.createContext<IThemeContext>(defaultState);
  • Consume the Context with useContext or add a contextType property to class components.

  • The TypeScript types will automatically be infered from the default values or the interface you provided.

Continue reading if you want to learn how to use React Context and get a more detailed explanation.

Create a new Context

As the first step, we need to create a new Context. I've taken as an example the ThemeContext I created for the dark mode feature of this blog.

We create the Context by using React's createContext function:

ThemeContext.tsx
import React, { useState, useEffect, createContext } from 'react';

const defaultState = {
  dark: false,
};

const ThemeContext = createContext(defaultState);

export default ThemeContext;

You need to install @types/react for the React typings to work.

Create a Context without default values

Even though this tutorial focuses on using React Context with TypeScript, it's not very different from plain JavaScript.

By using default values, React will infer the types without the need to define them ourselves.

If we don't want to use default values, however, we need to define an interface that defines the values of our Context.

interface IThemeContext {
  dark: boolean;
  toggleDark?: () => void;
}

We now need to pass this interface to createContext.

const ThemeContext = createContext<IThemeContext>({});
This doesn't work.

The code above throws a type error because the empty object is missing a property.

We can either define the missing default values or wrap the interface in the Partial helper function.

The Partial generic function tells TypeScript that we don't require all values to be defined.

const ThemeContext = createContext<Partial<IThemeContext>>({});

This works.

The issue with this approach is that all properties are marked as optional, forcing you to access every field safely through optional chaining (?.).

For this reason, it is better to avoid the Partial helper and define all required properties in the default state.

interface IThemeContext {
  dark: boolean;
  toggleDark?: () => void;
}

const defaultState = {
  dark: false,
};

const ThemeContext = React.createContext<IThemeContext>(defaultState);

Create a provider

The next thing we need to do is to wrap the part of our code that will make use of the Context in a Context provider.

App.tsx
import React from 'react';
import ThemeContext from '../context/ThemeContext';
// ...

export const App = () => {
  // ...

  return (
    <ThemeContext.Provider      value={{        dark: false,        toggleDark: () => console.log('toggle'),      }}    >      <ToggleDarkMode />
      {/*...*/}
    </ThemeContext.Provider>  );
};

The value prop allows us to set values that are different from the defaults.

While, in some cases, it makes sense to wrap your whole application in the Context provider, it's better only to wrap the code that needs the data since every update will re-render the provider's children.

We probably want the dark value to be dynamic, though.

The useState hook allows us to create a new state that will trigger a re-render of the child components when it changes.

App.tsx
export const App = () => {
  const [dark, setDark] = useState(defaultState.dark);  const toggleDark = () => {    setDark(!dark);  };
  return (
    <ThemeContext.Provider
      value={{        dark,        toggleDark,      }}    >
      <ToggleDarkMode />
      {/*...*/}
    </ThemeContext.Provider>
  );
};

Create a provider component

We may want to abstract the logic of our Context provider into its own component. This will make our code more organized, and we can easily reuse the Context provider if we need to.

ThemeContext.tsx
import React, { useState, FC } from 'react';

// ...

export const ThemeProvider: FC = ({ children }) => {
  const [dark, setDark] = useState(defaultState.dark);

  const toggleDark = () => {
    setDark(!dark);
  };

  return (
    <ThemeContext.Provider
      value={{
        dark,
        toggleDark,
      }}
    >
      {children}
    </ThemeContext.Provider>
  );
};

Now we need to use this component instead of ThemeContext.Provider to wrap around the code that will use the Context.

App.tsx
import React from 'react';
import { ThemeProvider } from '../context/ThemeContext';
export const App = () => {
  return (
    <ThemeProvider>      <ToggleDarkMode />
      {/*...*/}
    </ThemeProvider>  );
};

How to consume React Context

The last step is to consume the data that our Context provides.

I broke this section down into two parts. We'll need to use the useContext hook for our function components, and for class components, we just add a new class property.

How to use useContext with TypeScript

With the useContext hook, we don't need to add any additional typings as the hook will get the typings from the Context itself.

This means we only need to use the hook as we would in plain ReactJS:

ToggleDarkMode.tsx
const ToggleDarkMode = () => {
  const { dark, toggleDark } = useContext(ThemeContext);
  const handleOnClick = (e: MouseEvent<HTMLButtonElement>) => {
    e.preventDefault();
    toggleDark();
  };
  return (
    <>
      <h1>{dark ? "🌙" : "🌞"}</h1>
      <button onClick={handleOnClick}>Toggle dark mode</button>
    </>
  );
};

Please note that you can't use useContext in the same component where you use the Context provider because useContext gets the Context from its nearest parent.

Custom hooks

We can access the Context even easier by creating a custom hook that consumes the Context for us.

ThemeContext.tsx
export const useThemeContext = () =>  useContext(ThemeContext);

We can use this in our components like this:

ToggleDarkMode.tsx
const ToggleDarkMode = () => {
  const { dark, toggleDark } = useThemeContext();  // ...
};

Consume React Context in class components with TypeScript

In class components, we can't use React hooks. Here we need to add the Context type as a class property:

export class ToggleDarkMode extends React.Component {
  static contextType = ThemeContext;
  // ...
}

When a Context type is defined, React will assign the chosen Context to this.context.

export default class ToggleDarkMode extends React.Component {
  static contextType = ThemeContext;

  // ...
  render() {
    const { dark, toggleDark } = this.context;
    // ...
  }
}

As with useContext, TypeScript will infer the type from the default values or the interface we provided when creating the Context.

Conclusion

As you have seen, using React Context with TypeScript isn't much different to plain JavaScript. The React typings are intelligent enough to infer the types from the default values, and for more edge cases, we can always define our own interfaces.

I hope that this article helped you in understanding React Context. If you have any open questions or suggestions, feel free to send me an email or leave a comment.

Previous

React and TypeScript: How to find the right event type

2021-02-15 | 2 min
When getting started with TypeScript, finding the right interfaces can be challenging. In this article, you'll learn how to find the correct TypeScript interface for React events in any situation.

Support

PatreonKoFi