79

I have a Card component and a CardGroup component, and I'd like to throw an error when CardGroup has children that aren't Card components. Is this possible, or am I trying to solve the wrong problem?

daniula
  • 6,898
  • 4
  • 32
  • 49
bigblind
  • 12,539
  • 14
  • 68
  • 123

15 Answers15

68

For React 0.14+ and using ES6 classes, the solution will look like:

class CardGroup extends Component {
  render() {
    return (
      <div>{this.props.children}</div>
    )
  }
}
CardGroup.propTypes = {
  children: function (props, propName, componentName) {
    const prop = props[propName]

    let error = null
    React.Children.forEach(prop, function (child) {
      if (child.type !== Card) {
        error = new Error('`' + componentName + '` children should be of type `Card`.');
      }
    })
    return error
  }
}
Diego V
  • 6,189
  • 7
  • 40
  • 45
  • 21
    I don't know why, but ```child.type === Card``` isn't working in my setup. However I got it working by using ```child.type.prototype instanceof Card```. My React version is 15.5.4 – Veikko Karsikko Jul 11 '17 at 15:20
  • why not just throw it then and there, instead of returning an Error value. – user2167582 Sep 10 '17 at 19:03
  • 6
    @user2167582 Because that's the expected API for prop validation functions. [Official docs](https://github.com/facebook/prop-types/blob/master/README.md) include the following comment in example code: _You can also specify a custom validator. It should return an Error object if the validation fails. Don't `console.warn` or throw, as this won't work inside `oneOfType`_. – Diego V Sep 11 '17 at 11:05
  • 6
    Where's the docs of `child.type`? Can someone send link it please? – stevemao Sep 21 '17 at 07:50
  • this worked for me React.Children.forEach(props[propName], function (child) { if (child.type && child.type.name !== 'Card') { error = new Error('Blabla ´' + componentName + ' some error'); } }) – nerdess Feb 22 '18 at 13:12
  • 1
    @VeikkoKarsikko That's the only thing that worked for me. Thank you! This should be an answer, rather than a comment –  Apr 10 '18 at 10:29
  • @nerdess using child.type.name is not going to work in the minified production build...! – Leon Feb 22 '19 at 09:42
  • @VeikkoKarsikko ```instanceof``` should work when you're using class components, but will not when you're using function components. In the case of the latter, use the ```child.type === Card``` comparison. – Clinton Chau Dec 03 '20 at 20:19
  • Worth noting that `React.Children` iterates only through top-level children, so if you want to check the type of a child, you'll need to do so with `child.props.children`. – Nicolás de Ory Feb 01 '21 at 13:26
  • I'm on an MDX setup and only way I got it working was `child.props.mdxType === 'TabPanel'` – any_h Jan 05 '22 at 22:51
31

You can use the displayName for each child, accessed via type:

for (child in this.props.children){
  if (this.props.children[child].type.displayName != 'Card'){
    console.log("Warning CardGroup has children that aren't Card components");
  }  
}
MoeSattler
  • 6,684
  • 6
  • 24
  • 44
Mark
  • 3,113
  • 1
  • 18
  • 24
  • 5
    Make sure to check whether the current environment is development or production. propTypes validation is not triggered in production for performance. Using customProp in propTypes will be helpful. – Ryan Rho Apr 28 '15 at 23:43
  • 18
    You shouldn't do this because `props.children` is an [opaque data type](https://en.wikipedia.org/wiki/Opaque_data_type). Better use `React.Children` utilities as shown [here](http://stackoverflow.com/a/36424582/1385678). – Diego V Apr 06 '16 at 15:51
  • 34
    Keep in mind though, using things like Uglify will break this – MoeSattler May 12 '16 at 18:48
  • @MoeSattler is that true? I thought uglify would leave strings like that alone, because it can't know for certain whether they're being used elsewhere or not.. – jrz Jun 23 '17 at 01:42
  • 1
    The string won't change, but the Component's name might. – MoeSattler Jun 26 '17 at 16:44
  • 4
    As @RyanRho pointed out this can cause issue when running React in production mode. Instead I added a defaultProp to the child component e.g `static defaultProps = { isCard: true}` and then checked for this e.g `if (!this.props.children[child].props.isCard) { ... }` – Hedley Smith Jul 26 '17 at 12:37
  • 5
    That "helped me", but I used `name` instead of `displayName` (this last one doesn't worked for me) – ArCiGo Jan 23 '18 at 23:03
  • 11
    Never use the displayName since in production it might be stripped out! – HaNdTriX Feb 05 '18 at 14:52
  • Wouldn't you want to use `child.type.prototype instanceof Card` instead? Then minification/uglifying shouldn't cause an issue. – CTS_AE Nov 09 '20 at 22:45
  • 1
    Related: [How to get children type in react](https://stackoverflow.com/questions/39212960/how-to-get-children-type-in-react) (`name` might be a better option than `displayName`) – Henry Woody Feb 08 '21 at 19:55
16

For those using a TypeScript version. You can filter/modify components like this:

this.modifiedChildren = React.Children.map(children, child => {
            if (React.isValidElement(child) && (child as React.ReactElement<any>).type === Card) {
                let modifiedChild = child as React.ReactElement<any>;
                // Modifying here
                return modifiedChild;
            }
            // Returning other components / string.
            // Delete next line in case you dont need them.
            return child;
        });
SLCH000
  • 1,821
  • 2
  • 14
  • 15
15

You can use a custom propType function to validate children, since children are just props. I also wrote an article on this, if you want more details.

var CardGroup = React.createClass({
  propTypes: {
    children: function (props, propName, componentName) {
      var error;
      var prop = props[propName];

      React.Children.forEach(prop, function (child) {
        if (child.type.displayName !== 'Card') {
          error = new Error(
            '`' + componentName + '` only accepts children of type `Card`.'
          );
        }
      });

      return error;
    }
  },

  render: function () {
    return (
      <div>{this.props.children}</div>
    );
  }
});
cdpalmer
  • 658
  • 1
  • 7
  • 19
mzabriskie
  • 542
  • 1
  • 4
  • 10
11

Use the React.Children.forEach method to iterate over the children and use the name property to check the type:

React.Children.forEach(this.props.children, (child) => {
    if (child.type.name !== Card.name) {
        console.error("Only card components allowed as children.");
    }
}

I recommend to use Card.name instead of 'Card' string for better maintenance and stability in respect to uglify.

See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name

Salim
  • 2,446
  • 1
  • 14
  • 12
6

One has to use "React.isValidElement(child)" along with "child.type" if one is working with Typescript in order to avoid type mismatch errors.

React.Children.forEach(props.children, (child, index) => {
  if (React.isValidElement(child) && child.type !== Card) {
    error = new Error(
      '`' + componentName + '` only accepts children of type `Card`.'
    );
  }
});
Karna
  • 579
  • 6
  • 5
  • I have an error with this code: [ts] This condition will always return 'false' since the types 'string | ComponentClass<{}, any> | StatelessComponent<{}>' and 'typeof MyChildClass' have no overlap. [2367] – Frédéric Mascaro Jan 09 '19 at 10:14
5

You can add a prop to your Card component and then check for this prop in your CardGroup component. This is the safest way to achieve this in React.

This prop can be added as a defaultProp so it's always there.

class Card extends Component {

  static defaultProps = {
    isCard: true,
  }

  render() {
    return (
      <div>A Card</div>
    )
  }
}

class CardGroup extends Component {

  render() {
    for (child in this.props.children) {
      if (!this.props.children[child].props.isCard){
        console.error("Warning CardGroup has a child which isn't a Card component");
      }
    }

    return (
      <div>{this.props.children}</div>
    )
  }
}

Checking for whether the Card component is indeed a Card component by using type or displayName is not safe as it may not work during production use as indicated here: https://github.com/facebook/react/issues/6167#issuecomment-191243709

Hedley Smith
  • 1,307
  • 15
  • 12
  • 1
    This seems to be the only way I can get this working in a **production build**. I have a component that clones its children while adding extra props only to those of a certain type. – Palisand Oct 13 '17 at 02:47
4

I made a custom PropType for this that I call equalTo. You can use it like this...

class MyChildComponent extends React.Component { ... }

class MyParentComponent extends React.Component {
  static propTypes = {
    children: PropTypes.arrayOf(PropTypes.equalTo(MyChildComponent))
  }
}

Now, MyParentComponent only accepts children that are MyChildComponent. You can check for html elements like this...

PropTypes.equalTo('h1')
PropTypes.equalTo('div')
PropTypes.equalTo('img')
...

Here is the implementation...

React.PropTypes.equalTo = function (component) {
  return function validate(propValue, key, componentName, location, propFullName) {
    const prop = propValue[key]
    if (prop.type !== component) {
      return new Error(
        'Invalid prop `' + propFullName + '` supplied to' +
        ' `' + componentName + '`. Validation failed.'
      );
    }
  };
}

You could easily extend this to accept one of many possible types. Maybe something like...

React.PropTypes.equalToOneOf = function (arrayOfAcceptedComponents) {
...
}
Charlie Martin
  • 8,208
  • 3
  • 35
  • 41
4
static propTypes = {

  children : (props, propName, componentName) => {
              const prop = props[propName];
              return React.Children
                       .toArray(prop)
                       .find(child => child.type !== Card) && new Error(`${componentName} only accepts "<Card />" elements`);
  },

}
Abdennour TOUMI
  • 87,526
  • 38
  • 249
  • 254
3

I published the package that allows to validate the types of React elements https://www.npmjs.com/package/react-element-proptypes :

const ElementPropTypes = require('react-element-proptypes');

const Modal = ({ header, items }) => (
    <div>
        <div>{header}</div>
        <div>{items}</div>
    </div>
);

Modal.propTypes = {
    header: ElementPropTypes.elementOfType(Header).isRequired,
    items: React.PropTypes.arrayOf(ElementPropTypes.elementOfType(Item))
};

// render Modal 
React.render(
    <Modal
       header={<Header title="This is modal" />}
       items={[
           <Item/>,
           <Item/>,
           <Item/>
       ]}
    />,
    rootElement
);
vovacodes
  • 525
  • 4
  • 14
3

To validate correct children component i combine the use of react children foreach and the Custom validation proptypes, so at the end you can have the following:

HouseComponent.propTypes = {
children: PropTypes.oneOfType([(props, propName, componentName) => {
    let error = null;
    const validInputs = [
    'Mother',
    'Girlfried',
    'Friends',
    'Dogs'
    ];
    // Validate the valid inputs components allowed.
    React.Children.forEach(props[propName], (child) => {
            if (!validInputs.includes(child.type.name)) {
                error = new Error(componentName.concat(
                ' children should be one of the type:'
                    .concat(validInputs.toString())
            ));
        }
    });
    return error;
    }]).isRequired
};

As you can see is having and array with the name of the correct type.

On the other hand there is also a function called componentWithName from the airbnb/prop-types library that helps to have the same result. Here you can see more details

HouseComponent.propTypes = {
    children: PropTypes.oneOfType([
        componentWithName('SegmentedControl'),
        componentWithName('FormText'),
        componentWithName('FormTextarea'),
        componentWithName('FormSelect')
    ]).isRequired
};

Hope this help some one :)

Ismael Terreno
  • 1,021
  • 8
  • 5
3

Considered multiple proposed approaches, but they all turned out to be either unreliable or overcomplicated to serve as a boilerplate. Settled on the following implementation.

class Card extends Component {
  // ...
}

class CardGroup extends Component {
  static propTypes = {
    children: PropTypes.arrayOf(
      (propValue, key, componentName) => (propValue[key].type !== Card)
        ? new Error(`${componentName} only accepts children of type ${Card.name}.`)
        : null
    )
  }
  // ...
}

Here're the key ideas:

  1. Utilize the built-in PropTypes.arrayOf() instead of looping over children
  2. Check the child type via propValue[key].type !== Card in a custom validator
  3. Use variable substitution ${Card.name} to not hard-code the type name

Library react-element-proptypes implements this in ElementPropTypes.elementOfType():

import ElementPropTypes from "react-element-proptypes";

class CardGroup extends Component {
  static propTypes = {
    children: PropTypes.arrayOf(ElementPropTypes.elementOfType(Card))
  }
  // ...
}
3

An easy, production friendly check. At the top of your CardGroup component:

const cardType = (<Card />).type;

Then, when iterating over the children:

React.children.map(child => child.type === cardType ? child : null);

The nice thing about this check is that it will also work with library components/sub-components that don't expose the necessary classes to make an instanceof check work.

Jrd
  • 668
  • 6
  • 9
0

Assert the type:

props.children.forEach(child =>
  console.assert(
    child.type.name == "CanvasItem",
    "CanvasScroll can only have CanvasItem component as children."
  )
)
Joan
  • 4,079
  • 2
  • 28
  • 37
  • 1
    It's unreliable to iterate over the children prop assuming it's an array. It very well can be null or a single node. React provides handy utilities for handling children, such as React.Children.forEach(). See the official docs https://reactjs.org/docs/react-api.html#reactchildren – Sergii Shymko Mar 17 '21 at 01:27
0

Related to this post, I figured out a similar problem I had. I needed to throw an error if a child was one of many icons in a Tooltip component.

// icons/index.ts

export {default as AddIcon} from './AddIcon';
export {default as SubIcon} from './SubIcon';
...

// components/Tooltip.tsx

import { Children, cloneElement, isValidElement } from 'react';
import * as AllIcons from 'common/icons';
...
const Tooltip = ({children, ...rest}) => {
   Children.forEach(children, child => {
      // ** Inspired from this post
      const reactNodeIsOfIconType = (node, allIcons) => {
         const iconTypes = Object.values(allIcons);
         return iconTypes.some(type => typeof node === 'object' && node !== null && node.type === type);
      };
    
      console.assert(!reactNodeIsOfIconType(child, AllIcons),'Use  some other component instead...')
   })
   ...
   return Children.map(children, child => {
      if (isValidElement(child) {
         return cloneElement(child, ...rest);
      }
      return null;
   });
}
curtybear
  • 1,458
  • 2
  • 20
  • 39