Reputation: 175
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?
Upvotes: 10
Views: 22877
Reputation: 61
In my opinion, a satisfactory and easy solution is to create an additional function, that can setState. Example:
export const AuthContext = createContext(
{
checked: true,
loggedIn: false,
changeLoggedIn: (logged: boolean) => {}
});
const AuthContextProvider = (props: { children: React.ReactElement }) => {
const [checked, setchecked] = React.useState(true);
const [loggedIn, setloggedIn] = React.useState(false);
const changeLoggedIn = (logged: boolean) => {
setloggedIn(logged);
}
return (
<AuthContext.Provider value={{ loggedIn, checked, changeLoggedIn }}>
{props.children}
</AuthContext.Provider>
)
}
export default AuthContextProvider
Upvotes: 3
Reputation: 74490
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.
Upvotes: 46