Passing state with useContext in Typescript
Asked Answered
N

2

10

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?

Nadabus answered 1/10, 2019 at 23:17 Comment(2)
Try value={{state, setState}}Necrology
No joy. AppContext.tsx(8, 3): The expected type comes from property 'state' which is declared here on type '{ state: ObjectConstructor; setState: () => void; }Nadabus
F
45

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.

What went wrong?

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.


Solution

Let's assume, your desired state shape looks like:
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 = {/* ... */};

Discussing alternatives

Drop context default value completely (playground)

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!
}

Switch to useReducer and/or custom useAppContext Hook

Consider 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.

Faux answered 2/10, 2019 at 9:49 Comment(6)
Thanks so much! This really helped. I used the first example, the second may come in handy when I get to grips with Dispatch.Nadabus
Hi, I am also suffering from this error. but In your reply it is not clear to me how to use the AppProvider. Because there are not values given here. How can the components the state and setState values from the AppProvider? any help would be greatly appreciated.Mccombs
@Mccombs AppProvider can be used like this with MyComp (continuation of first example): const MyComp = () => { const {state,setState} = useContext(AppContext); ... } const App = () => <AppProvider> <MyComp /> </AppProvider>Faux
Hi Ford04! Thank you very much for your feedback! I really needed time to get it implemented ( yes I am a n00b in React and TypeScript) I finally got it to work!Mccombs
You're a god man, your explanation was on point!Matrilineage
I wanna set the state like this setState(prev => ({...prev, isMenuOpen: true})) but it gives me this error Parameter 'prev' implicitly has an 'any' type.(7006) Argument of type '(prev: any) => any' is not assignable to parameter of type 'AppContextState'.(2345) (parameter) prev: any pls suggest me how can I fix this?Gillen
L
3

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
Leis answered 5/1, 2023 at 19:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.