You can write in typeScript like this:
SomeContext.tsx:
Define your context and no need to define default value here.
import React, { createContext, useContext, useState } from 'react';
export interface SomeContextType {
someValue: string;
setSomeValue: React.Dispatch<React.SetStateAction<string>>;
}
const SomeContext = createContext<SomeContextType | undefined>(undefined);
export const useSomeContext = () => {
const context = useContext(SomeContext);
if (context === undefined) {
throw new Error('context not found');
}
return context;
};
SomeComponent.tsx:
Define your component that uses the context.
import { useSomeContext } from 'SomeContext';
const SomeComponent = () => {
const { someValue, setSomeValue } = useSomeContext();
// Rest of your component code
}
App.tsx
Wrap your application with the SomeContext.Provider.
import { SomeContext, SomeContextType } from 'SomeContext';
const App = () => {
const defaultValue: SomeContext = {
someValue: '',
setSomeValue: () => {}
};
const [someValue, setSomeValue] = useState<string>(defaultValue.someValue);
return (
<SomeContext.Provider value={{ someValue, setSomeValue }}>
// Rest of your code
</SomeContext.Provider>
);
}
I think this approach is commonly seen as the best way when working with React Context in TypeScript.
Answer from Moazzam Ahmed on Stack Overflowreactjs - Best way to use React Context (with useState) in Typescript - Stack Overflow
When to use context?
React Context vs Zustand: Am I Missing Out on Anything?
Is there a reason not to use Redux as a replacement for Context?
Videos
You can write in typeScript like this:
SomeContext.tsx:
Define your context and no need to define default value here.
import React, { createContext, useContext, useState } from 'react';
export interface SomeContextType {
someValue: string;
setSomeValue: React.Dispatch<React.SetStateAction<string>>;
}
const SomeContext = createContext<SomeContextType | undefined>(undefined);
export const useSomeContext = () => {
const context = useContext(SomeContext);
if (context === undefined) {
throw new Error('context not found');
}
return context;
};
SomeComponent.tsx:
Define your component that uses the context.
import { useSomeContext } from 'SomeContext';
const SomeComponent = () => {
const { someValue, setSomeValue } = useSomeContext();
// Rest of your component code
}
App.tsx
Wrap your application with the SomeContext.Provider.
import { SomeContext, SomeContextType } from 'SomeContext';
const App = () => {
const defaultValue: SomeContext = {
someValue: '',
setSomeValue: () => {}
};
const [someValue, setSomeValue] = useState<string>(defaultValue.someValue);
return (
<SomeContext.Provider value={{ someValue, setSomeValue }}>
// Rest of your code
</SomeContext.Provider>
);
}
I think this approach is commonly seen as the best way when working with React Context in TypeScript.
I agree, having to define (and maintain) default state is annoying (especially when there are several state values). I usually take the following approach:
import React, { PropsWithChildren, useCallback, useEffect, useState } from 'react';
export interface SomeContextValue {
someValue: string;
someFunction: () => void;
}
// I generally don't export the Context itself.
// I export 'SomeProvider' for creating the context and 'useSomeContext' hook to consume it.
// That way, we can skip the type checking here and verify the actual values later (if necessary).
const SomeContext = React.createContext<SomeContextValue>({} as SomeContextValue);
// The provider is responsible for managing its own state.
// If you want to reuse it in slightly different ways, pass some extra props to configure it.
export const SomeProvider: React.FC<PropsWithChildren> = (props) => {
// The state could be initialised via some default value from props...
// const [someValue, setSomeValue] = useState(props.defaultValue);
// ...or by some async logic inside a useEffect.
const [someValue, setSomeValue] = useState<string>();
useEffect(() => {
loadSomeValue().then(setSomeValue);
}, []);
// wrapping the state-mutation function in useCallback is optional,
// but it can stop unnecessary re-renders, depending on where the provider sits in the tree
const someFunction = useCallback(() => {
const nextValue = ''; // Some logic
setSomeValue(nextValue);
}, []);
// We ensure the state value actually exists before letting the children render
// If waiting for some data to load, we may render a spinner, text, or something useful instead of null
if (!someValue) return null;
return (
<SomeContext.Provider value={{ someValue, someFunction }}>
{props.children}
</SomeContext.Provider>
);
};
// This hook is used for consuming the context.
// I usually add a check to make sure it can only be used within the provider.
export const useSomeContext = () => {
const ctx = React.useContext(SomeContext);
if (!ctx) throw new Error('useSomeContext must be used within SomeProvider');
return ctx;
};
Note: much of this boilerplate can be abstracted into a helper/factory function (much like @Andrew's makeUseProvider) but we found that made it more difficult for developers to debug. Indeed, when you yourself revisit the code in 6-months time, it can be hard to figure out what's going on. So I like this explicit approach better.