網站搜尋

reactjs - 在 Typescript 中使用 useContext 傳遞狀態


我正在嘗試使用 useContext hook 將 state 和 setState 傳遞給子組件,但當我嘗試在 provider 的 value 參數中傳遞 [state, setState] 時卻遇到 ts 錯誤。我的程式碼如下:

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

我在設定 initialState 時,關於 value 變數出現了錯誤。

index.d.ts(290, 9): The expected type comes from property 'value' which is declared here on type 'IntrinsicAttributes & ProviderProps<{ state: ObjectConstructor; setState: () => void; }>'

我應該將初始狀態設置為什麼,以便我可以傳遞 state 和 useState 變量?

答案#1

TypeScript 從提供給 createContext 的 initialState 推斷 AppContext 類型。

AppContext.Provider 期望有一個 value 屬性,該屬性需符合上述類型。因此,由 createContext 實例化的類型決定了上下文的結構,使用的組件可以使用它。

出了什麼問題?

initialState 獲得以下推斷類型:

{ state: ObjectConstructor; setState: () => void; }

將 Object 傳遞給 state 意味著,你期望的是一個 ObjectConstructor —— 這不是真正你想要的。使用 setState:() => {} 時,組件無法以 state 參數來調用此函數。另請注意,useState 的初始值目前被額外包裝在一個陣列 [{...}] 中。

總結來說,[state, setState] 參數與 AppContext.Provider 的 value 屬性不相容。


解決方案

type AppContextState = { isMenuOpen: boolean; isSideOpen: boolean }
// omitting additional array wrapped around context value
// 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>
  );
};
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
};
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 和/或自訂 useAppContext Hook

考慮將 useState 替換為 useReducer,並將 dispatch 函數傳遞給子組件。這將提供更好的封裝,因為狀態操作邏輯現在集中在純粹的 reducer 中,子組件 無法 再通過 setState 直接操作它。

另一個將 UI 邏輯與領域邏輯分開的非常好替代方法是提供自訂的 useAppContext Hook,而不是使用 useContext(AppContext) —— 參見前面的範例。現在 useAppContext 可以提供更窄的 API,而無需公開整個上下文。

答案#2

在我看來,一個令人滿意且簡單的解決方法是創建一個額外的函數,該函數可以 setState。例如:

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