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 acontextType
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:
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>({});
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>>({});
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.
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> );
};
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.
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.
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.
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:
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.
export const useThemeContext = () => useContext(ThemeContext);
We can use this in our components like this:
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.