在 Typescript 中使用 useContext 传递状态

IT技术 reactjs typescript react-hooks
2021-03-29 03:21:37

我正在尝试使用 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 变量?

1个回答

TypeScriptAppContextinitialState给定到推断类型createContext

AppContext.Provider需要一个value与上述类型匹配props。所以实例化的类型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
然后具有适当类型的初始状态是(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>
  );
};
具有自己的上下文值类型(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 = {/* ... */};

讨论替代方案

完全删除上下文默认值(操场

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

考虑到更换useStateuseReducer并通过dispatch函数到组件。这将提供更好的封装,因为状态操作逻辑现在集中在一个纯 reducer 中,子组件不能再通过setState.

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

@g00golplexAppProvider可以像这样使用MyComp(第一个例子的延续): const MyComp = () => { const {state,setState} = useContext(AppContext); ... } const App = () => <AppProvider> <MyComp /> </AppProvider>
2021-05-25 03:21:37
嗨福特04!非常感谢您的反馈!我真的需要时间来实现它(是的,我是 React 和 TypeScript 的 n00b)我终于让它工作了!
2021-06-02 03:21:37
你好,我也遇到这个错误。但是在您的回复中,我不清楚如何使用 AppProvider。因为这里没有给出值。组件如何从 AppProvider 获取 state 和 setState 值?任何帮助将不胜感激。
2021-06-14 03:21:37
你是神人,你的解释很到位!
2021-06-15 03:21:37
非常感谢!这真的很有帮助。我使用了第一个示例,当我掌握 Dispatch 时,第二个示例可能会派上用场。
2021-06-16 03:21:37