Search code examples
javascriptreactjsreact-final-form

How to render React Final form fields in a loop


I'm trying to render multiple React Final Form fields in a loop. I have an array of objects as follows:

export const check_groups = [
    {
        name: "Mechanics",
        checks: [
            {typeName: "Boolean", title: "Clutch", icon: "fa fa-tire-rugged", key: "clutch", mandatory: true}
        ]
    },
    {
        name: "Maintenance & control",
        checks: [
            {typeName: "Boolean", title: "Recent timing belt", icon: "fal fa-cogs", key: "recent_timing_belt", mandatory: false},
            {typeName: "Boolean", title: "Recent service", icon: "fal fa-oil-can", key: "recent_service", mandatory: false}
        ]
    }
];

and I want to render a field for every object in that array. Thus I map through the array using lodash map function and render a new component as follows:

return check_groups.map(group => {
                return <div key={UUID.v4()}>
                    <div className="text-base w-full flex flex-row mb-1">
                        <div className="p-1 text-blue-dark">{group.name}</div>
                    </div>
                    {map(group.checks, (check) => {
                        switch (check.typeName) {
                            case "RegEx":
                                break;
                            case "Numeric":
                                break;
                            case "Boolean":
                                const name = `checks[${check.title}]`;
                                return (
                                    <TriStateItemField key={check.title} name={name} label={check.title} icon={check.icon} values={values}/>
                                );

                            default:
                                break;
                        }
                    })}
                </div>
            })

TriStateItemField is just an component which renders form field and pass the data to the presentation component (TriStateItem):

<Field name={name}
               validate={(validators) ? composeValidators(...validators) : false}
               data-tip={dataTip}
               data-for={dataFor}>

            {({input, meta}) => {
                return (
                    <TriStateItem label={label}
                                  name={name}
                                  type={type}
                                  className={className}
                                  style={style}
                                  hasError={meta.touched && meta.error}
                                  error={meta.error}
                                  placeholder={placeholder}
                                  disabled={disabled}
                                  helperText={helperText}
                                  icon={icon}
                                  {...input}
                                  {...meta}

                    />
                )
            }}
        </Field>

And TriStateItem is a component that shows the data:

class TriStateItem extends Component {
    renderValue(value){
        let new_value = undefined;
        if (value === "") new_value = true;
        if (value !== "" && value) new_value = false;
        if (value !== "" && !value) new_value = undefined;
        return new_value;
    }

    render() {
        const {label, icon, className, dataTip, dataFor, onChange, disabled, value, hasError, valid} = this.props;
        const isValid = value !== "";
        return (
            <div
                className={`flex flex-row justify-between w-full hover:bg-blue-200 bg-gray-100 cursor-pointer p-2 border-r-2 border-solid ${valid ? "border-transparent" : "border-red-500"} ${isValid ? "border-green-500" : ""} ${hasError ? "bg-red-100" : "bg-transparent" } ${className} ${disabled ? "opacity-60" : ""}`}
                onClick={() => onChange(this.renderValue(value))}>
                <div className="flex flex-row">
                    {icon !== undefined && <div className="pr-2"><i className={`text-gray-500 ${icon}`}/></div>}
                    <div>{label}</div>
                </div>
                {value === "" && <i className="fal fa-circle text-gray-500" style={{fontSize: 16}}/>}
                {value !== "" && value && <i className="fal fa-check text-green-500" style={{fontSize: 16}}/>}
                {value !== "" && !value && <i className="fal fa-times text-red-600" style={{fontSize: 16}}/>}
            </div>
        )
    }

The problem is that I get an error as follows:

Uncaught Invariant Violation: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.

and then:

The above error occurred in the <Field> component:
    in Field (at TriStateItemField.js:8)

Here is the example on sandbox. If you open the console you will see the same error.

Any idea what I do wrong?


Solution

  • I answered this on Github.

    I think that UUID library (I've never used it) is generating a new key for your div on every render, which causes infinite rendering because it's unregistering all your fields and reregistering them, which requires the whole form to rerender, which generates new keys, which unregisters/reregisters all your fields, which......

    Changing index.js:165 to <div key={group.name}> fixes it.