Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using instanceof to test for base class of a React component

To support nested navigation menu's, we're using React.cloneElement to add properties to child menu components (the menu components are custom components, based on react-bootstrap). To prevent that we're cloning all elements even though they are not child menu components, but regular content components, I want make the cloning conditional.

All menu components are sub classes of 'MenuBase' (which itself is a sub class of React.Component). In my code, I tried to test whether a child of a react component (reading this.props.children by use of the React.Children utility functions) is an instance of MenuBase.

Simplified code:

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

Somewhere in the menu logic I want to do something like the following

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

However, this test never results in true and as a result a clone is never made.

In the Chrome developer tools I can see that:

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

typescript child watch

Can somebody help me to clarify the following:

  1. Why is instanceof not working
  2. If I'm not able to use instance of the test for something in the inheritance chain of react components, what are my alternatives (I know I can test for the existence of one of the properties of IMenuBaseProps, but I don't really like that solution).
like image 804
Björn Boxstart Avatar asked Sep 08 '16 09:09

Björn Boxstart


People also ask

Why PropTypes is used in React?

PropTypes are simply a mechanism that ensures that the passed value is of the correct datatype. This makes sure that we don't receive an error at the very end of our app by the console which might not be easy to deal with.

What is an instance of a React component?

An instance is what you refer to as this in the component class you write. It is useful for storing local state and reacting to the lifecycle events. Function components don't have instances at all. Class components have instances, but you never need to create a component instance directly—React takes care of this.


2 Answers

Looking at the definition file, the ReactChildren.map is defined like this:

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

This ReactChild is then defined like this:

type ReactChild = ReactElement<any> | ReactText;

In your case it's probably ReactElement which is:

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

Your MenuBase is the type, so it probably should be:

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

It seems that the child.type is the class of the component and not the instance and so instead of doing child.type instanceof MenuBase you need to do child.type === MenuBase.


Edit

After playing around with things I came up with this solution:

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

If you'll want to check if the child is GeneralMenu then you'll need to do:

child.type.prototype === GeneralMenu.prototype
like image 51
Nitzan Tomer Avatar answered Nov 08 '22 02:11

Nitzan Tomer


The solution of @NitzanTomer did not seem to work under all circumstances. I was not able to find the cause of the difference in test results on his machine and my machine.

Finally I found the following solution:

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

In this solution, first a test is executed to check if the child is a React component. After that, 'instanceof' is used to determine whether the child component is a sub class of 'ComponentBase' (direct or indirect).

Because in TypeScript the 'type' property of React.Component does not have a 'prototype' property a cast to 'any' is necessary.

like image 40
Björn Boxstart Avatar answered Nov 08 '22 03:11

Björn Boxstart