bigblind
bigblind

Reputation: 12867

only allow children of a specific type in a react component

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?

Upvotes: 82

Views: 106929

Answers (15)

curtybear
curtybear

Reputation: 1538

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

Upvotes: 0

Jrd
Jrd

Reputation: 728

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.

Upvotes: 3

SLCH000
SLCH000

Reputation: 2223

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

Upvotes: 19

Sergii Shymko
Sergii Shymko

Reputation: 125

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

Upvotes: 3

Joan
Joan

Reputation: 4300

Assert the type:

props.children.forEach(child =>
  console.assert(
    child.type.name == "CanvasItem",
    "CanvasScroll can only have CanvasItem component as children."
  )
)

Upvotes: 0

Salim
Salim

Reputation: 2546

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

Upvotes: 11

Ismael Terreno
Ismael Terreno

Reputation: 1121

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

Upvotes: 3

Karna
Karna

Reputation: 649

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`.'
    );
  }
});

Upvotes: 6

mzabriskie
mzabriskie

Reputation: 542

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

Upvotes: 15

Hedley Smith
Hedley Smith

Reputation: 1417

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

Upvotes: 5

Abdennour TOUMI
Abdennour TOUMI

Reputation: 93173

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`);
  },

}

Upvotes: 4

vovacodes
vovacodes

Reputation: 525

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

Upvotes: 3

Charlie Martin
Charlie Martin

Reputation: 8406

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

Upvotes: 4

Mark
Mark

Reputation: 3163

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

Upvotes: 31

Diego V
Diego V

Reputation: 6414

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

Upvotes: 68

Related Questions