【问题标题】:How to create a generic React component with a typed context provider?如何使用类型化上下文提供程序创建通用 React 组件?
【发布时间】:2018-07-20 18:14:37
【问题描述】:

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

【问题讨论】:

    标签: reactjs typescript


    【解决方案1】:

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

    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 组件中大量使用泛型并不是一种很常见的风格,而且会导致代码相当混乱。

    【讨论】:

    • 我同意他的方法过于抽象。 React 几乎是一个好处,它使这变得困难,最终鼓励了一个更合适的解决方案。感谢您的回复。
    • List 的后代组件如何访问上下文ConsumerConsumer 本身是否需要作为道具向下传递组件树?这感觉有点违反直觉,因为上下文有助于避免在深树下放置一堆道具。
    【解决方案2】:

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

    希望对某人有所帮助

    【讨论】:

      【解决方案3】:

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

      让我们退后一步;上下文是通用的意味着什么?一些表示 Context 的 Producer 部分的组件 Producer&lt;T&gt; 可能只提供 T 类型的值,对吧?

      现在考虑以下几点:

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

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

      1. 如果Producer&lt;number&gt; 覆盖Producer&lt;string&gt;(即消费者得到123),泛型类型不会做任何事情。在 Producer 级别调用 number 并不能强制您在消费时获得 number,因此指定它是错误的希望。
      2. 如果两个生产者是完全独立的(即消费者得到"123"),它们必须来自两个独立的上下文实例,这些实例特定于它们所持有的类型。但是它们不是通用的!

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

      如何制作通用列表组件?

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

      最终,这意味着在定义上下文时使用any 之类的东西,并在使用该上下文时指定 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>
              );
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2023-02-13
        • 2022-10-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-01-24
        • 2019-01-01
        • 2020-03-24
        相关资源
        最近更新 更多