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:
Can somebody help me to clarify the following:
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.
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.
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
.
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With