I'm trying to use the useContext hook to pass state and setState to a child component but I'm getting a ts error when I try and pass [state, setState] in the value argument of the provider. My code is as follows:
export interface IProviderProps {
children?: any;
}
const initialState = {
state: Object,
setState: () => {},
};
export const AppContext = createContext(initialState);
export const AppProvider = (props: IProviderProps) => {
const [state, setState] = useState([{ isMenuOpen: false, isSideOpen: false }]);
return <AppContext.Provider value={[state, setState]}>{props.children}</AppContext.Provider>;
};
I'm getting an error on the value variable about the initialState
I'm setting.
index.d.ts(290, 9): The expected type comes from property 'value' which is declared here on type 'IntrinsicAttributes & ProviderProps<{ state: ObjectConstructor; setState: () => void; }>'
What do I set the initial state as to allow me to pass the state and useState variables?
You need to make the types match. Either just pass the context value from useState to the provider or change the type of your call to useContext to be an object with a string or null context property and a setter function.
It would be whatever type you used for the value of the context. If you have Context<T> then useContext returns T.
Using typescript To type the useState hook as an object in React, use the hook's generic, e.g. const [employee, setEmployee] = useState<{name: string; salary: number}>({name: '',salary: 0}) The state variable will only accept key-value pairs of the specified type.
TypeScript infers the AppContext
type from initialState
given to createContext
.
AppContext.Provider
expects a value
prop, that matches above type. So the type instantiated by createContext
determines the context shape, consuming components can use.
initialState
gets following inferred type:
{ state: ObjectConstructor; setState: () => void; }
Passing Object
to state
means, you expect an ObjectConstructor
- not really what you want. With setState: () => {}
, components are not able to invoke this function with a state
argument. Also note, useState
initial value is currently wrapped in an additional array [{...}]
.
In summary, [state, setState]
argument is incompatible to AppContext.Provider
value prop.
type AppContextState = { isMenuOpen: boolean; isSideOpen: boolean }
// omitting additional array wrapped around context value
Then an initial state with proper types is (playground):
// renamed `initialState` to `appCtxDefaultValue` to be a bit more concise
const appCtxDefaultValue = {
state: { isMenuOpen: false, isSideOpen: false },
setState: (state: AppContextState) => {} // noop default callback
};
export const AppContext = createContext(appCtxDefaultValue);
export const AppProvider = (props: IProviderProps) => {
const [state, setState] = useState(appCtxDefaultValue.state);
return (
// memoize `value` to optimize performance, if AppProvider is re-rendered often
<AppContext.Provider value={{ state, setState }}>
{props.children}
</AppContext.Provider>
);
};
A more explicit variant with own context value type (playground):
import { Dispatch, SetStateAction, /* and others */ } from "react";
type AppContextValue = {
state: AppContextState;
// type, you get when hovering over `setState` from `useState`
setState: Dispatch<SetStateAction<AppContextValue>>;
};
const appCtxDefaultValue: AppContextValue = {/* ... */};
export const AppContext = React.createContext<AppContextValue | undefined>(undefined);
export const AppProvider = (props: IProviderProps) => {
const [state, setState] = useState({ isMenuOpen: false, isSideOpen: false });
// ... other render logic
};
To prevent, that a client now has to check for undefined
, provide a custom Hook:
function useAppContext() {
const ctxValue = useContext(AppContext)
if (ctxValue === undefined) throw new Error("Expected context value to be set")
return ctxValue // now type AppContextValue
// or provide domain methods instead of whole context for better encapsulation
}
const Client = () => {
const ctxVal = useAppContext() // ctxVal is defined, no check necessary!
}
useReducer
and/or custom useAppContext
HookConsider to replace useState
by useReducer
and pass the dispatch
function down to components. This will provide better encapsulation, as the state manipulation logic is now centralized in a pure reducer and child components cannot manipulate it directly anymore via setState
.
Another very good alternative to separate UI Logic from domain logic is to provide a custom useAppContext
Hook instead of using useContext(AppContext)
- see previous example. Now useAppContext
can provide a more narrow API without publishing your whole context.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With