React Hooks - 使用 TypeScript 的 createContext 和 useReducer 時,無法將物件狀態和 dispatch 傳遞給 Provider
我正在嘗試使用 contextApi 和 hooks 來實現類似 redux 的行為,但我在使用 typescript 時遇到了一些問題。
我想要提供應用程式的狀態和 dispatch,以便能從元件內部存取和修改 store。問題是,當我使用 createContext 時,我仍然無法取得 state 或 dispatch 物件,因為我只能在 React 元件內呼叫 useReducer。因此,我只能宣告 createContext 時將參數的類型設為 any,以便當時可以傳入 null,之後再傳入其他的 dispatch 和 state。
import React, { createContext, useReducer } from 'react';
import reducerAuth, { initialState } from './reducerAuth';
const ContextAuth = createContext<any>(null);
const StateProvider = (
{ children }:{children:JSX.Element},
) => {
const { Provider } = ContextAuth;
const [state, dispatch] = useReducer(reducerAuth, initialState);
return (<Provider value={{ state, dispatch }}>{children}</Provider>);
};
export { ContextAuth, StateProvider };
有沒有辦法可以傳遞 state 和 dispatch,而不需要宣告類型為 any,並且不會有 TypeScript 衝突?
我不想使用 redux 來實現這一切,因為我想使用原生工具,但 redux 似乎更簡單,儘管非常相似。
我嘗試將物件 {state,dispatch} 的類型傳遞給 create context,如下所示:
const [state, dispatch] = useReducer(reducerAuth, initialState);
type UseReducer = {
state: typeof state,
dispatch: typeof dispatch,
}
const ContextAuth = createContext<UseReducer | null>(null);
但在這種情況下,我不再能夠導出 Context Auth,因為它隨後會在 StateProvider 函數內部被創建。
答案#1
你問題的根源在於,你試圖在物件被創建之後推斷它的類型,而不是理解將會創建哪種類型的物件。在我看來,這是一種倒退的設計。
在可能的情況下,最好避免使用 typeof。有些事情你可以用 type 或 interface 來說明,而 typeof 本身無法推斷清楚。string "myString" 可以是任何 string 值,還是它是一個字串字面量?這個 null 屬性是否只能是 null,還是它可能是 null 或其他某種類型?那其他類型會是哪一種呢?希望你明白我的意思。
dispatch 有一個已知的類型。它是一個接受 action 並返回 void 的函數。你不需要使用 typeof dispatch 來獲取它的類型。
type Dispatch<A> = (value: A) => void;
泛型變數 A 代表可接受的 action 參數。你使用哪一種取決於你希望你的應用程式的型別檢查有多嚴格。你可以將它設為你特定 action 類型的聯合集合,或者設為所有 action 都會實現的一般介面。Redux 匯出了通用型別 Action 和 AnyAction,但 React 沒有,所以你必須自己定義。
interface MyAction {
type: string;
payload: any;
}
type MyDispatch = Dispatch<MyAction>
同樣地,你應該已經知道你的 State 預期的類型/介面。如果不知道,就定義它吧!
這裡是一個完整的程式碼範例。要實現類型的地方是在你的 initialState 和 reducerAuth。如果這些類型設定正確,那麼 StateProvider 就不需要額外的類型,因為 useReducer Hook 可以推斷 state 和 dispatch 的類型。
import React, { createContext, useReducer } from 'react';
interface MyState {
// some actual data here
}
// initialState must fulfill the `MyState` interface
const initialState: MyState = {};
// just an example, you can define this type however you want
interface MyAction {
type: string;
payload: any;
}
// reducer declaration depends on the types for `MyState` and `MyAction`
const reducerAuth = (state: MyState, action: MyAction): MyState => {
// actual reducer here
return state;
};
// these are the values in your context object
interface MyContext {
state: MyState;
dispatch: React.Dispatch<MyAction>
}
const ContextAuth = createContext<MyContext | null>(null);
const StateProvider = ({ children }: { children: JSX.Element }) => {
const { Provider } = ContextAuth;
const [state, dispatch] = useReducer(reducerAuth, initialState);
return (
<Provider value={{ state, dispatch }}>
{children}
</Provider>
);
};
