Search code examples
reactjsreact-hooksreact-statereact-memo

React Nested Object State Using Memo


I pass in props to a function component:

const [accountForm, setAccountForm] = useState({})

const syncForm = (subForm, name) => {
  const form = {...accountForm, [name]: subForm}
  setAccountForm(form)
}

<>
  <ComponentA handler={syncForm} form={accountForm.objA} />
  <ComponentB handler={syncForm} form={accountForm.objB} />
  <ComponentC handler={syncForm} form={accountForm.objC} />
</>

The Components (A, B, & C) are written similarly like so:

const ComponentA = ({form, handler}) => {
  const handler = () {
    handler()
  }

  return (
    <Form onChange={handler}/>
  )
}

const areEqual = (n, p) => {
  const pForm = p.form;
  const nForm = n.form;
  const equal = pForm === nForm;
  return equal;
}

export default React.memo(ComponentA, areEqual)

I use a memo because Components A, B and C have the same parent, but I only want to rerender A, B or C when their subform has changed, so I don't want validations to run on other forms.

When I fill out form A, all the values are stored correctly. When I fill out a different form such as B or C, form A reverts to an empty object {}.

However, if I change the state to 3 objects instead of a nested object, it works:

const [objA, setObjA] = useState({});
const [objB, setObjB] = useState({});
const [objC, setObjC] = useState({});

<>
  <ComponentA form={objA} />
  <ComponentB form={objB} />
  <ComponentC form={objC} />
</>

Why doesn't the nested object approach work?


Solution

  • This seems to be a stale enclosure over the accountForm state. Looks like the memo HOC and areEqual function prevent the children components from rerendering and receiving a handler callback with updated state.

    Use a functional state update to correctly update from any previous state instead of whatever is closed over in any callback scope.

    const syncForm = (subForm, name) => {
      setAccountForm(form => ({
        ...form,
        [name]: subForm
      }));
    }