React: How to declare PropTypes for recursive data structures like a tree

Created on 16 Dec 2015  路  13Comments  路  Source: facebook/react

Hi,

I'd like to do something like:

var Node = PT.shape({
    value: ...,
    childrens: PT.arrayOf(Node).isRequired,
});

As you can guess this does not really work because it ends up calling PT.arrayOf(undefined).isRequired

I know this is probably a javascript limitation due do not implementing lazy evaluation, but is there a way to declare such structure properly in PropTypes? I've not seen this documented.

Most helpful comment

I think this works

var nodeShape = {
    value: ...
};
nodeShape.children = PT.arrayOf(PT.shape(nodeShape)).isRequired;

var Node = PT.shape(nodeShape);

All 13 comments

So it seems there's a workaround:

http://stackoverflow.com/questions/32063297/can-a-react-prop-type-be-defined-recursively

function lazyFunction(f) {
    return function () {
        return f().apply(this, arguments);
    };
}

var Node = PT.shape({
    value: ...,
    childrens: PT.arrayOf(lazyFunction(() => Node)).isRequired
});

This doesn鈥檛 seem like a very common use case, and can be arbitrarily slow so I don鈥檛 think it鈥檚 something we want to support in the core. I鈥檓 glad you found a solution that works for you! I鈥檓 closing but let us know if you have more thoughts on this.

This workaround triggers a react warning for calling PropTypes directly

It'd be nice if you could do something like this, but it looks like PropTypes.shape is not using the reference to the object during validation

const Node = {
  value: PropTypes.any
};

const NodeShape = PropTypes.shape(Node);

Node.children = PropTypes.arrayOf(NodeShape).isRequired;

I'm fine with it being arbitrarily slow since PropType checking is only in dev builds

This workaround triggers a react warning for calling PropTypes directly

Have you had a chance to read the page linked to from the warning? It describes exactly how to avoid false positives with it. 馃槈

Ah, I think I copied the example code wrong (I converted it to arrow functions by habit)-that secret argument should already be getting passed by the posted workaround via Function.prototype.apply. Thanks!

edit: Also it seems like the context (this) is relevant to the PropType function since I tried again to use arrow functions and it caused problems

Hmm I wouldn't expect that. Can you share a repro case?

Here is my recursive shape

import { PropTypes } from 'react';

// for nested proptypes
function lazyFunction(f) {
  return function () {
    return f().apply(this, arguments);
  };
}

let GameShape;

const TeamShape = PropTypes.shape({
  side: PropTypes.oneOf([ 'home', 'visitor' ]).isRequired,

  score: PropTypes.shape({
    score: PropTypes.number.isRequired
  }),

  seed: PropTypes.shape({
    displayName: PropTypes.string.isRequired,
    rank: PropTypes.number.isRequired,
    sourceGame: lazyFunction(() => GameShape)
  }),

  team: PropTypes.shape({
    id: PropTypes.string.isRequired,
    name: PropTypes.string.isRequired
  })
});

GameShape = PropTypes.shape({
  name: PropTypes.string.isRequired,
  teams: PropTypes.arrayOf(TeamShape).isRequired
});

export default GameShape;

Here is how I wrote lazyFunction using arrow functions the first time:

const lazyFunction = f => (() => f().apply(this, arguments));

I think it actually just had to do with 'arguments' not coming from the inner function after babel, since this version works:

const lazyFunction = f => ((...args) => f().apply(this, args));

@moodysalem Arrow functions don't have their own this or arguments.

I think this works

var nodeShape = {
    value: ...
};
nodeShape.children = PT.arrayOf(PT.shape(nodeShape)).isRequired;

var Node = PT.shape(nodeShape);

@jethrolarson Wouldn't it also be possible to switch the last 2 lines and use Node instead of writing .shape twice?

var nodeShape = {
    value: ...
};
var Node = PT.shape(nodeShape);
nodeShape.children = PT.arrayOf(Node).isRequired;

@jneuendorf Just tested it on PT 15.6.1 and it works

@jethrolarson Thanks for the solution!

@jesstelford I think this looks even better :+1:

const nodeShape = {
    value: PT.string,
    children: PT.arrayOf(PT.shape(this))
};

@SmolinPavel your approach actually doesn't work since this won't point out to the object nodeShape as I think you're assuming.

Instead:

var nodeShape = function () {
    return PT.shape({
        value: PT.string,
        children: PT.arrayOf(nodeShape)
    }).apply(this, arguments);
}
Was this page helpful?
0 / 5 - 0 ratings