1

I'm building a component that requires its direct descendants to be at least one of two types of components (ValidChildA or ValidChildB).

Using the most upvoted comment on this answer, I came up with this solution:

const ValidChildA = () => <div></div>;
const ValidChildB = () => <div></div>;

const Parent: React.FC<ParentProps> = ({ children, ...passedProps }) => {
    React.Children.forEach(children, child => {
        const isValidChildA = child.type.prototype instanceof ValidChildA;
        const isValidChildB = child.type.prototype instanceof ValidChildB;

        console.log(isValidChildA, isValidChildB); // Outputs false every time.
        if(!isValidChildA && !isValidChildB){
            console.error("Parent component expects only ValidChildA or ValidChildB as direct children.");
        }
    });

    return <div {...passedProps} />
};

export default function Page(){
    return (
        <Parent>
            <ValidChildA />
            <ValidChildB />
        </Parent>
    );
};

Now despite the children being valid, my error is still thrown and thus the component doesn't render correctly.

I could use the displayName property to check if a child is a valid component, but since Next uglifies the code and the displayName property in production, this would break the app.

GROVER.
  • 4,071
  • 2
  • 19
  • 66
  • Is the problem here that `instanceof` is looking for a class but the things you're returning are just functions? if you look at `child.type.prototype`, do you just get a `function`? – mr rogers Mar 30 '22 at 23:23
  • Side note - does it matter that you haven't closed the child tags? `` => ``? – mr rogers Mar 30 '22 at 23:25
  • @mrrogers When I output `child.type.prototype` it returns `{ constructor: f() }` - so it appears to be returning a class. But yes, I am using functional components. – GROVER. Mar 30 '22 at 23:27
  • Yeah - I guess with functional components, I just wonder if you'll ever know that the `constructor` is a `ValidChildA` type constructor since it's not a "proper" class. interesting problem... It looks like you're using Typescript? Do you need this check at runtime? or could you just - with typescript - define children to be of type `(ValidChildA | ValidChildB)[]` - or something like that. my typescript is a little rough – mr rogers Mar 30 '22 at 23:44
  • @mrrogers I am using TypeScript, although in a very basic manner because I only started learning it a couple of days ago. The check does need to be at run time. – GROVER. Mar 30 '22 at 23:46

1 Answers1

1

After a bunch of digging and reading (list included below), it seems like you could use named functions and then you could check the function name to see if it matched. I'm not sure if this will satisfy your requirements, but I did just try the following:

Define your ValidChild* components with function instead of the fat-arrow syntax () => .


function ValidChildA() {
   return <div>A</div>
}

function ValidChildB() { 
   return <div>B</div>
}

Then in your checks inside the children loop, you can look at the function name and make sure it's valid.

React.Children.forEach(children, (child) => {
   const isValidChildA = child.type?.name === ValidChildA.name;
   const isValidChildB = child.type?.name === ValidChildB.name;
   ...

With this code, this component does not log the error

<Parent>
  <ValidChildA />
  <ValidChildB />
</Parent>

But this one does

<Parent>
  <div>a</div>
  <ValidChildA />
  <ValidChildB />
</Parent>

The short answer ends here but I below I added some other thoughts and research that I did to get me here.

The reason what you're trying doesn't work (I think) is because functional components don't have a prototype - they are not classes - they are functions. So if you look at child.type.prototype, you'll find it's undefined which of course will never be an instanceof anything.

I did a bunch of experimenting and found that even if you moved to class based components, these checks don't work. Seems kind of strange but...

class ValidChildB extends React.Component {
  render() {
     return <div>Bprime</div>
    }
}

If we render this and add some logs...

React.Children.forEach(children, child => {
  console.log(`Prototype ${child.type.prototype}`);
  console.log(`instanceof object? ${child.type.prototype instanceof Object}`)
  console.log(`instanceof Component? ${child.type.prototype instanceof React.Component}`)
  console.log(`instanceof ValidChildB? ${child.type.prototype instanceof ValidChildB}`)
 
  ...
})

we get

// for ValidChildA - functional component
"Prototype undefined"
"instanceof object? false"
"instanceof Component? false"
"instanceof ValidChildB? false"

// for ValidChildB - class component
"Prototype [object Object]"
"instanceof object? true"
"instanceof Component? true"
"instanceof ValidChildB? false"

I did a bunch of reading and a Typescript guard solution (telling typescript to enforce that the children are ValidChildA | ValidChildB is tricky or not doable. Also, as you pointed out, doesn't solve your issue because you need it to happen at runtime. But still found it an interesting little research project. Check out the posts if you want more on that direction.

mr rogers
  • 3,200
  • 1
  • 19
  • 31
  • 1
    I just re-read your above notes and I wonder if this solution *also* falls short because of 'uglification' ? :( – mr rogers Mar 31 '22 at 02:40
  • Gonna upvote anyway, since you went through the effort of research and made a really detailed answer for any future people who may run into this problem without the issue of uglifications :-) – GROVER. Mar 31 '22 at 02:52
  • If you find another solution, i'd love to know. It was a super fun research project. It exposes (sort of) one of the things I don't like about typescript which is that it's *not* run-time. In typescript, i'd really like to be able to do `if (thing.type == "One") { ... } else if (thing.type == 'Two') { ... } ` but because it's all compile time, it doesn't really let you do that. – mr rogers Mar 31 '22 at 03:29
  • A quick afterthought - uglification may not be a problem - if the function is renamed "Ugly()", the check should also be modified to `child.type.name === Ugly.name` which means it might just work :D – mr rogers Mar 31 '22 at 03:34