3

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} />
});
MeatPopsicle
  • 832
  • 13
  • 28
  • Is SingleLineStringInput memoized? For a function component, you can import `memo` from React and wrap your component in it on export (HOC style). If it's a class component, you can extend PureComponent instead of Component. – Jay Kariesch Oct 21 '19 at 15:11
  • You could likely use useMemo from React as well to memoize the JSX array to see if that does the trick, albeit I'm just spit-balling so I have no guarantee that would be the solution, since it seems you're needing to do a deep compare. – Jay Kariesch Oct 21 '19 at 15:15
  • Thanks for the replies. SingleLineStringInput is a functional component and I tried wrapping it in React.memo but it didn't seem to help. – MeatPopsicle Oct 21 '19 at 17:16
  • Switching to React.useReducer() seems to have done the trick, it's not perfectly lag free now but it seems like the other inputs are at least no longer updating as I type – MeatPopsicle Oct 22 '19 at 07:21
  • 1
    @MeatPopsicle I have posted an answer, hope it helps you – ibtsam Oct 22 '19 at 07:45

1 Answers1

1

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

ibtsam
  • 1,620
  • 1
  • 10
  • 10