The trick to not having to babysit the context types is easy!
If you use the context API, then one problem is the babysitting of its types.
Another one is having to use multiple imports to use it when you need it.
With this example, we solve both problems and make it quick and easy to use the React Context API.
Copy Paste Example
Copy, paste, then just replace all “example” to what you need to name and you're ready to go.
(Afterwards, there will be a fully commented version.)
import {
createContext,
useCallback,
useContext,
useDeferredValue,
useMemo,
useState,
} from 'react';
function useContextValue(init: number = 0) {
const [state, setState] = useState(init);
const doubleValue = state * 2;
const defferedStringValue = useDeferredValue(state.toString());
const reset = useCallback(() => {
setState(init);
}, []);
const value = useMemo(
() => ({
state,
doubleValue,
defferedStringValue,
reset,
}),
[
state,
doubleValue,
defferedStringValue,
reset,
],
);
return value;
}
type ExampleContext = ReturnType<typeof useContextValue>;
const Context = createContext<ExampleContext>(null!);
Context.displayName = 'ExampleContext';
export function ExampleContextProvider({
children,
initValue = 0,
}: {
children: React.ReactNode;
initValue?: number;
}) {
const value = useContextValue(initValue);
return <Context.Provider value={value}>{children}</Context.Provider>;
}
export function useExample() {
const value = useContext(Context);
if (!value) {
throw new Error('useExample must be used within a ExampleContextProvider');
}
return value;
}
Commented Version
import {
createContext,
useCallback,
useContext,
useDeferredValue,
useMemo,
useState,
} from 'react';
/**
* We create a custom hook that will have everything
* that would usually be in the context main function
*
* this way, we can use the value it returns to infer the
* type of the context
*/
function useContextValue(init: number = 0) {
// do whatever you want inside
const [state, setState] = useState(init);
const doubleValue = state * 2;
const defferedStringValue = useDeferredValue(state.toString());
// remember to memoize functions
const reset = useCallback(() => {
setState(init);
}, []);
// and also memoize the final value
const value = useMemo(
() => ({
state,
doubleValue,
defferedStringValue,
reset,
}),
[
state,
doubleValue,
defferedStringValue,
reset,
],
);
return value;
}
/**
* Since we can infer from the hook,
* no need to create the context type by hand
*/
type ExampleContext = ReturnType<typeof useContextValue>;
const Context = createContext<ExampleContext>(null!);
Context.displayName = 'ExampleContext';
export function ExampleContextProvider({
children,
/**
* this is optional, but it's always a good to remember
* that the context is still a react component
* and can receive values other than just the children
*/
initValue = 0,
}: {
children: React.ReactNode;
initValue?: number;
}) {
const value = useContextValue(initValue);
return <Context.Provider value={value}>{children}</Context.Provider>;
}
/**
* We also export a hook that will use the context
*
* this way, we can use it in other components
* by importing just this one hook
*/
export function useExample() {
const value = useContext(Context);
/**
* this will throw an error if the context
* is not used within the provider
*
* this also avoid the context being "undefined"
*/
if (!value) {
throw new Error('useExample must be used within a ExampleProvider');
}
return value;
}
Final Words
That's it. Context API is easier and more subtilized than it should, but it’s a powerful tool for the cases where it needs to be used.
Just do remember that the React Context API is not Redux (or other state managers) and you shouldn’t shove the whole application state in it.
Well, you can, but it can cause unnecessary problems.
This was written with React < 19 in mind, with the new compiler coming, memorization and unnecessary rendering might not cause problems.
Top comments (0)