在 Typescript 中使用 useContext 传递状态 [英] Passing state with useContext in Typescript

查看:615
本文介绍了在 Typescript 中使用 useContext 传递状态的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试使用 useContext 钩子将 state 和 setState 传递给子组件,但是当我尝试在提供程序的 value 参数中传递 [state, setState] 时出现 ts 错误.我的代码如下:

<预><代码>导出接口 IProviderProps {孩子?:有;}常量初始状态 = {状态:对象,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): 预期的类型来自属性value",它在类型IntrinsicAttributes &"上声明.ProviderProps<{ state: ObjectConstructor;setState: () =>空白;}>'

如何设置初始状态以允许我传递 state 和 useState 变量?

解决方案

TypeScript 从给 createContextinitialState 推断 AppContext 类型.

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

出了什么问题?

initialState 得到以下推断类型:

{ state: ObjectConstructor;setState: () =>空白;}

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

总而言之,[state, setState] 参数与 AppContext.Provider 值属性不兼容.


解决方案

让我们假设,您想要的状态形状如下所示:

type AppContextState = { isMenuOpen: boolean;isSideOpen: 布尔值 }//省略围绕上下文值的附加数组

然后用适当的类型的初始状态是(游乐场):

//将 `initialState` 重命名为 `appCtxDefaultValue` 以使其更加简洁const appCtxDefaultValue = {状态: { isMenuOpen: false, isSideOpen: false },setState: (state: AppContextState) =>{}//noop 默认回调};export const AppContext = createContext(appCtxDefaultValue);export const AppProvider = (props: IProviderProps) =>{const [state, setState] = useState(appCtxDefaultValue.state);返回 (//memoize `value` 以优化性能,如果 AppProvider 经常重新渲染<AppContext.Provider value={{ state, setState }}>{props.children}</AppContext.Provider>);};

与自己的上下文值A型更明确的变体(游乐场):

import { Dispatch, SetStateAction,/* and others */} from "react";类型 AppContextValue = {状态:AppContextState;//输入,当你从 `useState` 悬停在 `setState` 上时得到setState: Dispatch>;};const appCtxDefaultValue: AppContextValue = {/* ... */};


讨论替代方案

删除上下文默认值完全(游乐场)

export const AppContext = React.createContext(未定义);export const AppProvider = (props: IProviderProps) =>{const [state, setState] = useState({ isMenuOpen: false, isSideOpen: false });//... 其他渲染逻辑};

为了防止客户端现在必须检查 undefined,请提供自定义 Hook:

function useAppContext() {const ctxValue = useContext(AppContext)if (ctxValue === undefined) throw new Error(要设置的预期上下文值")return ctxValue//现在输入 AppContextValue//或提供域方法而不是整个上下文以获得更好的封装}const 客户端 = () =>{const ctxVal = useAppContext()//ctxVal 已定义,无需检查!}

切换到 useReducer 和/或自定义 useAppContext Hook

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

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

I'm trying to use the useContext hook to pass state and setState to a child component but I'm getting a ts error when I try and pass [state, setState] in the value argument of the provider. My code is as follows:


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

I'm getting an error on the value variable about the initialState I'm setting.

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

What do I set the initial state as to allow me to pass the state and useState variables?

解决方案

TypeScript infers the AppContext type from initialState given to createContext.

AppContext.Provider expects a value prop, that matches above type. So the type instantiated by createContext determines the context shape, consuming components can use.

What went wrong?

initialState gets following inferred type:

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

Passing Object to state means, you expect an ObjectConstructor - not really what you want. With setState: () => {}, components are not able to invoke this function with a state argument. Also note, useState initial value is currently wrapped in an additional array [{...}].

In summary, [state, setState] argument is incompatible to AppContext.Provider value prop.


Solution

Let's assume, your desired state shape looks like:

type AppContextState = { isMenuOpen: boolean; isSideOpen: boolean }
// omitting additional array wrapped around context value

Then an initial state with proper types is (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>
  );
};

A more explicit variant with own context value type (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 = {/* ... */};


Discussing alternatives

Drop context default value completely (playground)

export const AppContext = React.createContext<AppContextValue | undefined>(undefined);

export const AppProvider = (props: IProviderProps) => {
    const [state, setState] = useState({ isMenuOpen: false, isSideOpen: false });
    // ... other render logic
};

To prevent, that a client now has to check for undefined, provide a custom 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!
}

Switch to useReducer and/or custom useAppContext Hook

Consider to replace useState by useReducer and pass the dispatch function down to components. This will provide better encapsulation, as the state manipulation logic is now centralized in a pure reducer and child components cannot manipulate it directly anymore via setState.

Another very good alternative to separate UI Logic from domain logic is to provide a custom useAppContext Hook instead of using useContext(AppContext) - see previous example. Now useAppContext can provide a more narrow API without publishing your whole context.

这篇关于在 Typescript 中使用 useContext 传递状态的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆