Kousha
Kousha

Reputation: 36219

At least one required prop in React

I need to make at least one of the props required:

MyComponent.propTypes = {
   data: PropTypes.object,
   url: PropTypes.string
};

So in the example above, either data or url prop must be provided. The use case here is that the user could either provide the data or the url. If the url is provided, then the component will fetch the data.

Bonus question: How do I do at least one prop vs only one of the props?

Upvotes: 43

Views: 18501

Answers (5)

Steven Koch
Steven Koch

Reputation: 797

   
function requireALeastOne(checkProps) {
  return function(props, propName, compName) {
    const requirePropNames = Object.keys(checkProps);

    const found = requirePropNames.find((propRequired) => props[propRequired]);

    try {
      if (!found) {
        throw new Error(
          `One of ${requirePropNames.join(',')} is required by '${compName}' component.`,
        );
      }
      PropTypes.checkPropTypes(checkProps, props, propName, compName);
    } catch (e) {
      return e;
    }
    return null;
  };
}


const requireALeast = requireALeastOne({
  prop1: PropTypes.string,
  prop2: PropTypes.number
});

Comp.propTypes = {
  prop1: requireALeast,
  prop2: requireALeast
};

Upvotes: 2

thelastshadow
thelastshadow

Reputation: 3654

I wrote this helper to solve the same problem in a re-usable way. You use it like a propType function:

MyComponent.propTypes = {
  normalProp: PropType.string.isRequired,

  foo: requireOneOf({
    foo: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.number
    ]),
    bar: PropTypes.string,
  }, true),
};

and in this example it ensures one of foo or bar is in the MyComponent props. If you leave out the second argument it'll ensure only one of foo or bar is passed.

/**
 * Takes a propTypes object ensuring that at least one of the passed types
 * exists on the component.
 *
 * Usage:
 *
 * MyComponent.propTypes = {
 *   normalProp: PropType.string.isRequired,
 *
 *   foo: requireOneOf({
 *     foo: PropTypes.oneOfType([
 *       PropTypes.string,
 *       PropTypes.number
 *     ]),
 *     bar: PropTypes.string,
 *   }, true),
 * };
 *
 * @param requiredProps object
 * @param allowMultiple bool = false  If true multiple props may be
 *                                    passed to the component
 * @return {Function(props, propName, componentName, location)}
 */
export const requireOneOf = (requiredProps, allowMultiple = false) => {
  return (props, propName, componentName, location) => {
    let found = false;

    for (let requiredPropName in requiredProps) {
      if (requiredProps.hasOwnProperty(requiredPropName)) {
        // Does the prop exist?
        if (props[requiredPropName] !== undefined) {
          if (!allowMultiple && found) {
            return new Error(
              `Props ${found} and ${requiredPropName} were both passed to ${componentName}`
            );
          }

          const singleRequiredProp = {};
          singleRequiredProp[requiredPropName] = requiredProps[requiredPropName];
          const singleProp = {};
          singleProp[requiredPropName] = props[requiredPropName];

          // Does the prop match the type?
          try {
            PropTypes.checkPropTypes(singleRequiredProp, singleProp, location, componentName);
          } catch (e) {
            return e;
          }
          found = requiredPropName;
        }
      }
    }

    if (found === false) {
      const propNames = Object.keys(requiredProps).join('", "');
      return new Error(
        `One of "${propNames}" is required in ${componentName}`
      );
    }
  };
};

Upvotes: 2

U Rogel
U Rogel

Reputation: 1941

Adding on top of finalfreq answer and relating to kousha comment to it.

I had a button component that required either icon or title. Make sure at least one of the is there like in the above answer, after that check its type can be validated like so:

Button.propTypes = {
  icon: (props, propName, componentName) => {
    if (!props.icon && !props.title) {
      return new Error(`One of props 'icon' or 'title' was not specified in '${componentName}'.`)
    }
    if (props.icon) {
      PropTypes.checkPropTypes({
        icon: PropTypes.string, // or any other PropTypes you want
      },
      { icon: props.icon },
      'prop',
      'PrimaryButtonWithoutTheme')
    }
    return null
  }
  title: // same process
}

For more info about PropTypes.checkPropTypes read here

Upvotes: 4

Beau Smith
Beau Smith

Reputation: 34367

A more concise version of @finalfreq's solution:

const requiredPropsCheck = (props, propName, componentName) => {
  if (!props.data && !props.url) {
    return new Error(`One of 'data' or 'url' is required by '${componentName}' component.`)
  }
}

Markdown.propTypes = {
  data: requiredPropsCheck,
  url: requiredPropsCheck,
}

Upvotes: 31

finalfreq
finalfreq

Reputation: 6980

PropTypes actually can take a custom function as an argument so you could do something like this:

MyComponent.propTypes = {
  data: (props, propName, componentName) => {
    if (!props.data && !props.url) {
      return new Error(`One of props 'data' or 'url' was not specified in '${componentName}'.`);
    }
  },

  url: (props, propName, componentName) => {
    if (!props.data && !props.url) {
      return new Error(`One of props 'url' or 'data' was not specified in '${componentName}'.`);
    }
  }
}

which allows for customer Error messaging. You can only return null or an Error when using this method

You can find more info on that here

https://facebook.github.io/react/docs/typechecking-with-proptypes.html#react.proptypes

from the react docs:

// 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`.
  customProp: function(props, propName, componentName) {
    if (!/matchme/.test(props[propName])) {
      return new Error(
        'Invalid prop `' + propName + '` supplied to' +
        ' `' + componentName + '`. Validation failed.'
      );
    }
  },

Upvotes: 49

Related Questions