如何使用类型化上下文提供程序创建通用React组件?

时间:2018-07-20 18:14:37

标签: reactjs typescript

使用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,而我们不必动态创建使用者和提供者。

3 个答案:

答案 0 :(得分:4)

我不知道TypeScript,所以我不能用相同的语言回答,但是如果您希望您的Provider是MyList类的“特定”对象,则可以在同一函数中创建两者。 / p>

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组件中大量使用泛型不是很常见的样式,并且可能导致相当混乱的代码。

答案 1 :(得分:4)

我遇到了同样的问题,我想我以一种更为优雅的方式解决了它: 您可以使用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>();
     ...
}

希望它对某人有帮助

答案 2 :(得分:0)

不幸的是,我认为答案是这个问题实际上没有道理。

让我们退后一步;上下文是通用的意味着什么?某些表示上下文生产者一半的组件Producer<T>可能只提供类型T的值,对吧?

现在考虑以下几点:

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

这应该如何表现?消费者应该获得什么价值?

  1. 如果Producer<number>覆盖Producer<string>(即消费者获得123),则通用类型不会执行任何操作。在生产者级别将其称为number并不会强制您在使用时会得到number,因此指定它存在错误的希望。
  2. 如果两个生产者都打算完全分开(即,消费者获得"123"),则它们必须来自两个单独的Contexts实例,这些实例特定于它们所拥有的类型。但是然后它们不是通用的!

在任何一种情况下,直接将类型传递给Producer都是没有价值的。这并不是说当Context在起作用时,泛型是无用的...

如何制作通用列表组件?

作为使用通用组件已有一段时间的人,我不认为您的列表示例过于抽象。只是您无法在生产者和消费者之间强制执行类型协议-就像您无法“强制”从Web请求,本地存储或第三方代码中获得的值的类型一样!

最终,这意味着在定义上下文时使用any之类的东西,而在使用该上下文时指定期望的类型。

示例

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