Search code examples
reactjsrenderingreact-state-management

Re-rendering only updated elements on react form


I want to achieve a dynamically built form with controlled components that scale well. The problem I'm having is updating the state is causing the entire form to re-render (or something expensive?) causing epic type lag.

// Generate some example fields
let defaultFields = {};
for(let n=0; n<1000; n++){
    defaultFields[n.toString()] = {'id':n.toString(), 'value':'','label':'Field '+n}
}
const [valuesById, setValuesById] = useState(defaultFields);

const updateValueCallback = React.useCallback((e)=>{
    e.preventDefault();
    e.persist();
    setValuesById(prevValuesById => {
        let fieldId = e.target.id;
        return {...prevValuesById,
            [fieldId]:{
                'id':fieldId,
                'value':e.target.value,
                'label':'Field '+fieldId
        }};
    });
});
return <div>
    { Object.entries(valuesById).map(([id,formField]) => {
        return <p key={formField.id}>
            <label>{formField.label}
                <SingleLineStringInput isRequired={false} value={formField.value} onChangeCallback={updateValueCallback} id={formField.id} name={'name_'+formField.id} />
            </label>
        </p>
    })
    }
</div>;

If the props aren't changing for 999 of the fields then why do they re-render? Or what is actually happening here (the fields don't actually flash in the debug tool but the parent does)? I really need help to understand this better and a fix which isn't too drastically different from what I've done as we've built a large amount of logic on top of this basic structure and have only now realised that it's not scaling.

SingleLineInput:

const SingleLineStringInput = React.memo(({name, id, value, onChangeCallback}) => {
    console.log("UPDATING "+id);
    return <input className={'input ' + inputClasses.join(' ')} name={name} id={id} type="text"
                  value={(value === null) ? '' : value}
                  onChange={onChangeCallback} />
});

Solution

  • Ok I will try to help, you are doing fine using memo and useCallback, but you are not passing the array of dependencies to useCallback, as your callback would be same for every render, you can pass an empty array to it, so it will be the same function on every render, You can do it as this

    const updateValueCallback = React.useCallback((e)=>{
    e.preventDefault();
    e.persist();
    setValuesById(prevValuesById => {
        let fieldId = e.target.id;
        return {...prevValuesById,
            [fieldId]:{
                'id':fieldId,
                'value':e.target.value,
                'label':'Field '+fieldId
        }};
    });
    }, []); // dependencies array
    

    Now if you change a field value on that one will be re-rendered

    Sandbox to show you how its done

    Hope it helps