Search code examples
javascriptreactjsreact-state-managementuse-context

React useContext losing state


I have a problem that I've been trying to solve for the past couple of days:

I'm using a Context Provider for form fields and for whatever reason fields keep overwriting each other when I use memo.

Provider:

export const Context = React.createContext();

function Form_Provider({ values, onChange, children }) {
  return (
    <Context.Provider value={{ values, onChange }}>
      {children}
    </Context.Provider>
  )
});
export default Form_Provider

Field:

function Field({ label, name }) {
  const { values, onChange } = useContext(Context);
  return (
      <Memorable
        label={label}
        onChange={({ target: t }) => onChange({ name, value: t.value })}
        value={values[name]}
      />
  );


}

const Memorable = React.memo(props => {
  return (
      <Form.Item label={props.label}>
        <Input
          value={props.value}
          onChange={props.onChange}
        />
      </Form.Item>
    </>
  )
}, ({ value: newValue}, { value: oldValue }) => newValue == oldValue)

Form

const [formValues, setFormValues] = useState({ field1: 'Foo', field2: 'Bar' });

<Form.Provider
  values={formValues}
  onChange={({ name, value }) => setFormValues({...formValues, [name]: value }))
>
  <Form.Field name='field1' label="Field 1" />
  <Form.Field name='field2' label="Field 2" />
</Form.Provider>

(Tried to simplify it as much as possible)

In my actual code I've added a json print prettifier to track the state for every field and it works out until every single field when i only edit one field. However, once I start editing another field the first field I've edited goes back to its original state and/or some other weird in between state from it's past.

If I dont use Memo it works but that can't be the solution as I'll be working with a lot of fields and that would cause a lot of re-rendering.

Anyone any idea what's going on here?

Addition:

I've already tried using an internal reducer for this and passing down a dispatch function. As long as I don't try to manage the state outside of the provider everything works.


Solution

  • I'm pretty sure the issue is that you are memoizing the original values of formValues from here:

    onChange={({ name, value }) => setFormValues({...formValues, [name]: value }))
    

    Say field1 has a change in it's value. It calls the onChange handler, which merges ...formValues - the values that existed when the component mounted - with the new value for field1.

    Now the equality function in React.memo for field1 returns false, because the value is different. That particular field re-renders to recieve its new value, and also the new values of ...formValues. The other fields, however, have not rerendered. For them, ...formValues still means the value of the state as it existed the last time they re-rendered, which was when the component mounted.

    If you now change the value of field2, it will set the state to the result of merging the original state with the new value of field2. Hence field1 is reset because its value has now changed again back to the original value.

    A simple solution to this would be to use the callback version of setState, which always uses the state's current value:

    onChange={({ name, value }) => setFormValues(fv => {...fv, [name]: value }))
    

    However, I would be tempted not to do this, and instead get rid of the memoisation altogether. This is because your equality function does not actually accurately reflect the way that props provided to the component change. I believe the performance gains here are also negligible, because the component is so small and does not render any additional components itself.

    Assuming there's no animation tied to the value change, it is very cheap to perform and does not make a good candidate for memoisation, which also escapes the built in React optimisation. You should think carefully to decide if you really need it before implementing it.