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
