reactjs 如何创建一个带有类型化上下文提供者的通用React组件?

c9x0cxw0  于 2023-01-25  发布在  React
关注(0)|答案(4)|浏览(175)

使用React的新上下文API,您可以创建一个类型化的上下文生产者/消费者,如下所示:

type MyContextType = string;

const { Consumer, Producer } = React.createContext<MyContextType>('foo');

但是,假设我有一个列出项目的泛型组件。

// To be referenced later
interface IContext<ItemType> {
    items: ItemType[];
}

interface IProps<ItemType> {
    items: ItemType[];
}

class MyList<ItemType> extends React.Component<IProps<ItemType>> {
    public render() {
        return items.map(i => <p key={i.id}>{i.text}</p>);
    }
}

如果我想把某个定制组件呈现为列表项,并从MyList传入属性作为上下文,我该如何实现呢?
我尝试过的:

方法#1

class MyList<ItemType> extends React.Component<IProps<ItemType>> {
    // The next line is an error.
    public static context = React.createContext<IContext<ItemType>>({
        items: []
    }
}

这种方法不起作用,因为您不能从静态上下文访问类的类型,这是有意义的。

方法#2

使用标准上下文模式,我们在模块级别创建消费者和生产者(即不在类内部)。这里的问题是我们必须在知道它们的类型参数之前创建消费者和生产者。

方法#3

我发现a post on Medium反映了我所要做的事情。交换的关键是,在我们知道类型信息之前,我们不能创建生产者/消费者(似乎很明显,对吗?)。

class MyList<ItemType> extends React.Component<IProps<ItemType>> {
    private localContext: React.Context<IContext<ItemType>>;

    constructor(props?: IProps<ItemType>) {
        super(props);

        this.localContext = React.createContext<IContext<ItemType>>({
            items: [],
        });
    }

    public render() {
        return (
            <this.localContext.Provider>
                {this.props.children}
            </this.localContext.Provider>
        );
    }
}

这(也许)是一个进步,因为我们可以示例化正确类型的提供者,但是子组件如何访问正确的消费者呢?

更新

正如下面的答案所提到的,这种模式是试图过度抽象的一个标志,这在React中不太好用。如果要尝试解决这个问题,我会创建一个泛型ListItem类来封装项目本身。这样,上下文对象可以被类型化为任何形式的ListItem,我们不必动态创建消费者和提供者。

mklgxw1f

mklgxw1f1#

我不知道TypeScript,所以我不能用同一种语言回答,但是如果你想让你的Provider“特定于”你的MyList类,你可以在同一个函数中创建这两个Provider。

function makeList() {
  const Ctx = React.createContext();

  class MyList extends Component {
    // ...
    render() {
      return (
        <Ctx.Provider value={this.state.something}>
          {this.props.children}
        </Ctx.Provider>
      );
    }
  }

  return {
    List,
    Consumer: Ctx.Consumer 
  };
}

// Usage
const { List, Consumer } = makeList();

总的来说,我认为你可能过于抽象了。在React组件中大量使用泛型不是一种很常见的风格,可能会导致相当混乱的代码。

ccgok5k5

ccgok5k52#

我也遇到过同样的问题,我想我用了一种更优雅的方式解决了它:您可以使用lodash once(或者自己创建一个,这非常简单)使用泛型类型初始化上下文,然后从函数内部调用它,在其余组件中,您可以使用自定义useContext钩子来获取数据:

父组件:

import React, { useContext } from 'react';
import { once } from 'lodash';

const createStateContext = once(<T,>() => React.createContext({} as State<T>));
export const useStateContext = <T,>() => useContext(createStateContext<T>());

const ParentComponent = <T>(props: Props<T>) => {
    const StateContext = createStateContext<T>();
    return (
        <StateContext.Provider value={[YOUR VALUE]}>
            <ChildComponent />
        </StateContext.Provider>
    );
}

子组件:

import React from 'react';
import { useStateContext } from './parent-component';

const ChildComponent = <T>(props: Props<T>) => {
     const state = useStateContext<T>();
     ...
}

希望能帮到什么人

v440hwme

v440hwme3#

不幸的是,我认为答案是这个问题实际上没有意义。
让我们退一步说对于一个通用的上下文来说,这意味着什么?一些代表生产者的上下文的组件Producer<T>可能只提供T类型的值,对吗?
现在考虑以下内容:

<Producer<string> value="123">
  <Producer<number> value={123}>
    <Consumer />
  </Producer>
</Producer>

这应该如何表现?消费者应该得到什么样的价值?
1.如果Producer<number>覆盖Producer<string>(即消费者得到123),泛型类型不做任何事情。在生产者级别调用它number并不强制你在消费时得到number,所以指定它是徒劳的。
1.如果两个生产者是完全独立的(例如,消费者得到"123"),那么它们必须来自两个独立的上下文示例,这两个示例特定于它们所拥有的类型。
在这两种情况下,将类型直接传递给Producer都没有任何价值。这并不是说当上下文起作用时泛型就没用了...

如何创建通用列表组件?

作为一个已经使用了一段时间泛型组件的人,我不认为你的列表示例过于抽象,只是你不能强制生产者和消费者之间的类型协议--就像你不能“强制”从Web请求、本地存储或第三方代码中获得的值的类型一样!
最终,这意味着在定义Context时使用类似any的内容,并在使用该Context时指定一个 expected 类型。

示例

const listContext = React.createContext<ListProps<any>>({ onSelectionChange: () => {} });

interface ListProps<TItem> {
  onSelectionChange: (selected: TItem | undefined) => void;
}

// Note that List is still generic! 
class List<TItem> extends React.Component<ListProps<TItem>> {
    public render() {
        return (
            <listContext.Provider value={this.props}>
                {this.props.children}
            </listContext.Provider>
        );
    }
}

interface CustomListItemProps<TItem> {
  item: TItem;
}

class CustomListItem<TItem> extends React.Component<CustomListItemProps<TItem>> {
    public render() {
        // Get the context value and store it as ListProps<TItem>.
        // Then build a list item that can call onSelectionChange based on this.props.item!
    }
}

interface ContactListProps {
  contacts: Contact[];
}

class ContactList extends React.Component<ContactListProps> {
    public render() {
        return (
            <List<Contact> onSelectionChange={console.log}>
                {contacts.map(contact => <ContactListItem contact={contact} />)}
            </List>
        );
    }
}
dddzy1tm

dddzy1tm4#

检查Formik如何给药

对我来说,在使用上下文(useContext)时检查类型很重要,就像Formik useFormikContext一样。

泛型类型示例

type YourContextType<T> = { state: T; setState: (s: T) => void }

创建上下文(不关心类型)

const YourContext = createContext<YourContextType<any>>(
  undefined as any,
);

具有泛型类型的上下文挂接

function useYourContext<T>() {
  const context = useContext<YourContextType<T>>(YourContext);

  if (context === undefined) {
    // assert if context is available
    throw new Error('No context provided');
  }

  return context;
}

使用钩子

const { state, changeState } = useYourContext<{ name: string }>();

你会看到state和changeState都有类型。也许它不是你要找的100%,但对我来说已经足够了。查看@TheRubberDuck的解释
源代码:https://github.com/jaredpalmer/formik/blob/master/packages/formik/src/FormikContext.tsx

相关问题