There are 3 options I've seen in the wild.
Option 1
This option is a bit annoying because the type of StateContext value can be null | StateContextType. But in the code you provided, it will only be null on creation and not null inside the provider. Regardless, everywhere you use useContext you'll have to have a guard against null.
Copyimport React, { createContext, useState } from 'react';
type StateContextType = {
activeMenu: boolean;
setActiveMenu: React.Dispatch<React.SetStateAction<boolean>>;
};
export const StateContext = createContext<null | StateContextType>(null);
type ContextProviderProps = {
children: React.ReactNode;
};
export const ContextProvider = ({ children }: ContextProviderProps) => {
const [activeMenu, setActiveMenu] = useState(true);
const value = {
activeMenu,
setActiveMenu,
};
return (
<StateContext.Provider value={value}>{children}</StateContext.Provider>
);
};
Option 2
With this option, you cast StateContext value type to StateContextType. It means that the value of StateContext is assumed to be StateContextType. The only downside is that the value of StateContext is null for a very small amount of time at creation, before a value is provided in StateContext.Provider.
However, it's relatively safe since you're immediately passing a value in the provider.
Copyimport React, { createContext, useState } from 'react';
type StateContextType = {
activeMenu: boolean;
setActiveMenu: React.Dispatch<React.SetStateAction<boolean>>;
};
export const StateContext = createContext<StateContextType>(
null as unknown as StateContextType,
);
type ContextProviderProps = {
children: React.ReactNode;
};
export const ContextProvider = ({ children }: ContextProviderProps) => {
const [activeMenu, setActiveMenu] = useState(true);
const value = {
activeMenu,
setActiveMenu,
};
return (
<StateContext.Provider value={value}>{children}</StateContext.Provider>
);
};
Option 3
Credit to Steve Kinney's course on Frontend Masters on React and TypeScript for this one.
Copyimport React, { useState } from 'react';
export function createContext<T>() {
const context = React.createContext<T | undefined>(undefined);
const useContext = () => {
const value = React.useContext(context);
if (value === undefined) {
throw new Error(
`useContext must be used inside a Provider with a value that's not undefined`,
);
}
return value;
};
return [useContext, context.Provider] as const;
}
type StateContextType = {
activeMenu: boolean;
setActiveMenu: React.Dispatch<React.SetStateAction<boolean>>;
};
export const [useContext, Provider] = createContext<StateContextType>();
type ContextProviderProps = {
children: React.ReactNode;
};
export const ContextProvider = ({ children }: ContextProviderProps) => {
const [activeMenu, setActiveMenu] = useState(true);
const value = {
activeMenu,
setActiveMenu,
};
return <Provider value={value}>{children}</Provider>;
};
const Component = () => {
// usage inside component
const context = useContext();
return <div></div>;
};
export const App = () => {
return (
<ContextProvider>
<Component />
</ContextProvider>
);
};
Answer from Spyros Argalias on Stack OverflowThere are 3 options I've seen in the wild.
Option 1
This option is a bit annoying because the type of StateContext value can be null | StateContextType. But in the code you provided, it will only be null on creation and not null inside the provider. Regardless, everywhere you use useContext you'll have to have a guard against null.
Copyimport React, { createContext, useState } from 'react';
type StateContextType = {
activeMenu: boolean;
setActiveMenu: React.Dispatch<React.SetStateAction<boolean>>;
};
export const StateContext = createContext<null | StateContextType>(null);
type ContextProviderProps = {
children: React.ReactNode;
};
export const ContextProvider = ({ children }: ContextProviderProps) => {
const [activeMenu, setActiveMenu] = useState(true);
const value = {
activeMenu,
setActiveMenu,
};
return (
<StateContext.Provider value={value}>{children}</StateContext.Provider>
);
};
Option 2
With this option, you cast StateContext value type to StateContextType. It means that the value of StateContext is assumed to be StateContextType. The only downside is that the value of StateContext is null for a very small amount of time at creation, before a value is provided in StateContext.Provider.
However, it's relatively safe since you're immediately passing a value in the provider.
Copyimport React, { createContext, useState } from 'react';
type StateContextType = {
activeMenu: boolean;
setActiveMenu: React.Dispatch<React.SetStateAction<boolean>>;
};
export const StateContext = createContext<StateContextType>(
null as unknown as StateContextType,
);
type ContextProviderProps = {
children: React.ReactNode;
};
export const ContextProvider = ({ children }: ContextProviderProps) => {
const [activeMenu, setActiveMenu] = useState(true);
const value = {
activeMenu,
setActiveMenu,
};
return (
<StateContext.Provider value={value}>{children}</StateContext.Provider>
);
};
Option 3
Credit to Steve Kinney's course on Frontend Masters on React and TypeScript for this one.
Copyimport React, { useState } from 'react';
export function createContext<T>() {
const context = React.createContext<T | undefined>(undefined);
const useContext = () => {
const value = React.useContext(context);
if (value === undefined) {
throw new Error(
`useContext must be used inside a Provider with a value that's not undefined`,
);
}
return value;
};
return [useContext, context.Provider] as const;
}
type StateContextType = {
activeMenu: boolean;
setActiveMenu: React.Dispatch<React.SetStateAction<boolean>>;
};
export const [useContext, Provider] = createContext<StateContextType>();
type ContextProviderProps = {
children: React.ReactNode;
};
export const ContextProvider = ({ children }: ContextProviderProps) => {
const [activeMenu, setActiveMenu] = useState(true);
const value = {
activeMenu,
setActiveMenu,
};
return <Provider value={value}>{children}</Provider>;
};
const Component = () => {
// usage inside component
const context = useContext();
return <div></div>;
};
export const App = () => {
return (
<ContextProvider>
<Component />
</ContextProvider>
);
};
change this:
Copyimport React, {createContext, useContext, useState } from 'react';
const StateContext = createContext();
export const ContextProvider = ({ children }) => {
const [activeMenu, setActiveMenu] = useState(true);
return (
<StateContext.Provider value={{activeMenu, setActiveMenu}}>
{children}
</StateContext.Provider>
)
}
export const useStateContext = () => useContext(StateContext)
to this:
Copyimport React, {createContext, useContext,
useState } from 'react';
interface Props {
children : ReactNode
}
const StateContext = createContext();
export const ContextProvider = ({ children }:Props) => {
const [activeMenu, setActiveMenu] = useState(true);
return (
<StateContext.Provider value={{activeMenu, setActiveMenu}}>
{children}
</StateContext.Provider>
)
}
export const useStateContext = () => useContext(StateContext)
How does TypeScript help make useContext data more discoverable?
What is the useContext hook in React?
What is the problem with useContext data in plain JavaScript?
Videos
Hi, i'm trying to correctly type (or maybe refactor) react's contextAPI in my app.
I use classic flow:
const myContext = createContext<MyContextObj | null>(null); // at first i create empty context.
function MyProvider...
// magic happens and i got my myContextObj object (in my case it contain `dispatch` from `useReducer()`, but it doesnt matter)
return
<myContext.Provider value={myContextObj}> // At this point i provide actual context value.
...The problem is, that context consumer assumes that context might me null (well, it was null at the moment of definition)
function Consumer... const dispatchFn = useContext(myContext) dispatchFn(...) // Typescript: Beware! dispatchFn can be null! Boo
What is a correct way of dealing with it?
// it works if i just: const dispatchFn = useContext(myContext) as MyContextObj // but maybe there is more appropriate way?
First of all, you declare the context type like so:
export const Context = createContext<UserContextType | undefined>(undefined);
This means that the value of the context is either an object with all properties present of UserContextType or it is undefined.
Then here:
const [state, setState] = useState<IUser>();
You create state that has a type of IUser. But because you do not provide a default value, the default value is undefined. This is added to the state's type for you. So state is of type IUser | undefined.
Then we get here:
value={{state, setState}}
state is IUser | undefined, but the context type expects an IUser[] for that property. So neither IUser or undefined are valid types there.
First, you need to make sure that if you are passing in an object, then the object is correctly typed. That means that it has all required properties present. Else, pass in undefined. You can do this with a ternary expression:
value={state ? {state, setState} : undefined}
Now if state has a value, then the context value is created and passed it. Otherwise the context value is undefined.
But now you get this error:
Types of property 'state' are incompatible.
Type 'IUser' is missing the following properties from type 'IUser[]': length, pop, push, concat, and 26 more.(2322)
This is because you trying to assign a single user IUser to an array of users IUser[].
It looks like you mean for the content to only hold a single user, so you probably want to change the contexts type to:
export type UserContextType = {
state: IUser;
setState: (newSession: IUser) => void;
};
Which works, see playground
Or you need to pass in an array of users:
const UserProvider: React.FC<React.ReactNode> = ({ children }) => {
const [state, setState] = useState<IUser[]>([]);
return (
<Context.Provider value={{state, setState}}>{children}</Context.Provider>
)
};
Which also works, see playground
You could also change the context type to:
export type UserContextType = {
state?: IUser;
setState: (newSession: IUser) => void;
};
Which makes state optional, and then:
const UserProvider: React.FC<React.ReactNode> = ({ children }) => {
const [state, setState] = useState<IUser>();
return (
<Context.Provider value={{state, setState}}>{children}</Context.Provider>
)
};
Should work fine, see playground
This is an example of a Context on Typescript
AuthContext.tsx:
import { createContext, ReactNode, useContext, useState } from "react";
import { AuthContextData } from "../models/AuthContextData.model";
const AuthContext = createContext<AuthContextData>({} as AuthContextData);
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<object | null>(null);
const [loading, setLoading] = useState<boolean>(true);
async function signIn(): Promise<void> {
console.log('sign in')
}
async function signOut(): Promise<void> {
console.log('sign out')
}
return (
<AuthContext.Provider
value={{ signed: !!user, user, signIn, signOut, loading }}
>
{children}
</AuthContext.Provider>
);
};
export function useAuth() {
const context = useContext(AuthContext);
return context;
}
AuthContextData.ts:
export interface AuthContextData {
signed: boolean;
user: object | null;
signIn(): Promise<void>;
signOut(): Promise<void>;
loading: boolean;
}