首页 > 解决方案 > 在 Typescript 中使用 useContext 传递状态

问题描述

我正在尝试使用 useContext 挂钩将 state 和 setState 传递给子组件,但是当我尝试在提供程序的 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关于我正在设置的值变量,我收到错误消息。

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 变量?

标签: reactjstypescriptreact-hooks

解决方案


TypeScript 将AppContext类型从initialStategiven 推断为createContext.

AppContext.Provider需要一个value与上述类型匹配的道具。所以实例化的类型createContext决定了上下文形状,消费组件可以使用。

什么地方出了错?

initialState得到以下推断类型:

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

传递Objectstate意味着,你期望ObjectConstructor- 不是你想要的。使用setState: () => {},组件无法使用state参数调用此函数。另请注意,useState初始值当前包含在一个附加数组[{...}]中。

总之,[state, setState]argument 与AppContext.Providervalue prop 不兼容。


解决方案

让我们假设,您想要的状态形状如下所示:
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
};
为了防止,客户端现在必须检查undefined,提供一个自定义 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和/或自定义useAppContextHook

考虑替换useStatebyuseReducer并将dispatch函数向下传递给组件。这将提供更好的封装,因为状态操作逻辑现在集中在纯 reducer 中,子组件不能再通过setState.

将 UI 逻辑与域逻辑分开的另一个很好的替代方法是提供自定义useAppContextHook 而不是使用useContext(AppContext)- 请参阅前面的示例。现在useAppContext可以在不发布整个上下文的情况下提供更窄的 API。


推荐阅读