使用 instanceof 测试 React 组件的基类

IT技术 reactjs typescript
2021-05-22 00:18:19

为了支持嵌套导航菜单,我们使用 React.cloneElement 向子菜单组件添加属性(菜单组件是基于 react-bootstrap 的自定义组件)。为了防止我们克隆所有元素,即使它们不是子菜单组件,而是常规内容组件,我想让克隆有条件。

所有菜单组件都是“MenuBase”的子类(它本身是 React.Component 的子类)。在我的代码中,我尝试测试 React 组件的子组件(通过使用 React.Children 实用程序函数读取 this.props.children)是否是 MenuBase 的实例。

简化代码:

interface IMenuBaseProps {
  // menu related properties
}

abstract class MenuBase<P extends IMenuBaseProps, S> extends React.Component<P, S> {
  // constructor etc.
}

interface IGeneralMenuProps extends IMenuBaseProps {
  // additional properties
}

class GeneralMenu extends MenuBase<IGeneralMenuProps, {}> {
  public render(): JSX.Element {
    // do some magic
  }
}

在菜单逻辑的某个地方,我想做如下事情

React.Children.map(this.props.children, (child: React.ReactElement<any>): React.ReactElement<any> ==> {
  if (child instanceof MenuBase) {
    // return clone of child with additional properties
  } else {
    // return child
  }
}

但是,此测试永远不会结果为真,因此永远不会进行克隆。

在 Chrome 开发人员工具中,我可以看到:

  1. child.type = 函数 GeneralMenu(props)
  2. child.type.prototype = MenuBase

typescript儿童手表

有人可以帮我澄清以下问题:

  1. 为什么 instanceof 不起作用
  2. 如果我无法将测试实例用于react组件继承链中的某些内容,那么我的替代方案是什么(我知道我可以测试 IMenuBaseProps 的属性之一是否存在,但我真的不喜欢该解决方案)。
2个回答

查看定义文件ReactChildren.map定义如下:

map<T>(children: ReactNode, fn: (child: ReactChild, index: number) => T): T[];

然后这个ReactChild定义如下:

type ReactChild = ReactElement<any> | ReactText;

在您的情况下,它可能是ReactElement,它是:

interface ReactElement<P> {
    type: string | ComponentClass<P> | SFC<P>;
    props: P;
    key?: Key;
}

MenuBasetype,所以它可能应该是:

React.Children.map(this.props.children, (child: React.ReactElement<any>): React.ReactElement<any> ==> {
    if (child.type === MenuBase) {
        // return clone of child with additional properties
    } else {
        // return child
    }
}

似乎child.type是组件的类而不是实例,因此child.type instanceof MenuBase您无需执行此操作child.type === MenuBase


编辑

在玩弄东西之后,我想出了这个解决方案:

React.Children.map(this.props.children, (child: React.ReactElement<any>): React.ReactElement<any> ==> {
    if (MenuBase.isPrototypeOf(child.type)) {
        // return clone of child with additional properties
    } else {
        // return child
    }
}

如果您想检查是否child是,GeneralMenu那么您需要执行以下操作:

child.type.prototype === GeneralMenu.prototype

@NitzanTomer 的解决方案似乎并非在所有情况下都有效。我无法找到他的机器和我的机器上测试结果差异的原因。

最后我找到了以下解决方案:

public render(): JSX.Element {
    // Iterate over children and test for inheritance from ComponentBase
    let results: JSX.Element[] = React.Children.map(this.props.children, (child: React.ReactElement<any>, index: number): JSX.Element => {
        let isComponent: boolean = typeof child.type !== 'string' && React.Component.prototype.isPrototypeOf((child.type as any).prototype);
        let result: boolean = isComponent ? (child.type as any).prototype instanceof ComponentBase : false; // Not a component, then never a ComponentBase
        return <li>
                <div>Child nr. {index} is a React component: {isComponent ? "True" : "False"}</div>
                <div>And is a sub class of ComponentBase: {result ? "True" : "False"} { result !== this.props.expectedTestResult ? "(unexpected)" : ""}</div>
            </li>;
    })

    return (
        <ul>
            {results}
        </ul>
    )
}

在此解决方案中,首先执行测试以检查子组件是否为 React 组件。之后,'instanceof' 用于确定子组件是否是 'ComponentBase' 的子类(直接或间接)。

因为在 TypeScript 中,React.Component 的 'type' 属性没有 'prototype' 属性,所以必须转换为 'any'。