Search code examples
reactjsrecursioncyclereact-proptypes

Cyclical/Recursive React PropTypes


Not to be confused with a similar question asking how to define PropTypes recursively, I'm looking for a way to pass in an argument that itself is recursive without:

  • wasting cycles checking stuff that has already been checked and
  • throwing "Warning: Failed prop type: Maximum call stack size exceeded" messages due to the checker recurring until it dies.

Here's a simplistic example of how this might be used.

const fields = [{
        accessor: 'firstName',
        type: 'text',
        required: true,
        label: 'First Name',
        placeholder: 'Enter your first name'
}];

fields.push({
    accessor: 'children',
    type: 'dynamicList',
    label: 'Children',
    fields: fields
});

return (
    <Form 
        fields={fields} 
        onSave={data => console.log(data)} />
);

One hacky workaround I can think of immediately is to instead pass some sort of identifier instead that the component uses to look up some referenced prop. Is there another way, without changing React's typechecker to prevent cycles?

Note: For those interested, doing something like above works fine as far as I can tell, save for the log warnings and potentially some slight performance impact.


Solution

  • One way I came up with to handle prop cycles on recursive PropTypes is an adjustment to the lazy evaluation method, like so:

    function lazyFunction(f, _lazyCheckerHasSeen) {
        return function() {
            if (_lazyCheckerHasSeen.indexOf(arguments[0]) != -1) {
                return true;
            }
    
            _lazyCheckerHasSeen.push(arguments[0]);
            return f().apply(this, arguments);
        }
    }
    

    Used like so, for the aforementioned example:

    const lazyFieldType = lazyFunction(function () { 
        return fieldType;
    }, []);
    
    const fieldType = PropTypes.shape({
        accessor: PropTypes.string.isRequired,
        type: PropTypes.oneOf(['text', 'multitext', 'checkbox', 'select', 'multiselect', 'section', 'dynamicList']).isRequired,
        required: PropTypes.boolean,
        label: PropTypes.string,
        placeholder: PropTypes.string,
        fields: PropTypes.arrayOf(lazyFieldType),
    });
    
    Form.propTypes = {
        fields: PropTypes.arrayOf(fieldType),
        onSave: PropTypes.func.isRequired
    };