ReactJS, “key” is the key to solve this unable to update issue

2 minute read

In the project I am working on, there is a poorly finished Address control from a library maintained by another team. The control manages its own address state, which cannot be updated by simply passing a different value prop on re-render. Worse, the control doesn’t expose a method via ref to update the value in the classic JavaScript manner either. That makes it tricky for the consumer side, typically a form component, to update the value programmatically.

The first workaround I tried was a HOC (higher order component). There was an existing HOC for the Address control anyway, and it was applied in the module scope in the source:

const WrappedAddress = withAddressHOC(Address)

function FormUsingAddress(props) {
  const { updatedAddress } = props
  // updatedAddress is only respected once, on mount
  return <WrappedAddress name="propertyAddress" value={updatedAddress} />
}

I moved that withAddressHOC call into the component body, so that on every render a new wrapped Address component would be created and mounted with the current value from props.

function FormUsingAddress(props) {
  const WrappedAddress = withAddressHOC(Address)

  const { updatedAddress } = props
  // a brand new component type every render, so it remounts and picks up the value
  return <WrappedAddress name="propertyAddress" value={updatedAddress} />
}

That solves the “value not updating” issue, but as we all know, calling a HOC inside the render body is an anti-pattern. It creates a new component type on every render, which throws away and rebuilds the entire subtree each time. In my project the Address control makes a few API calls on mount, so this produces a huge number of API calls as the form keeps updating and re-rendering.

Finally, I consulted Claude about this issue. Thanks to Claude, it showed me a much better way to work around it. Instead of abusing the HOC, we should use the key prop. The key prop is not only about the annoying warning you see when you forget to pass it for a mapped array of elements. It also has an important feature: React remounts a component when its key changes. That remount is exactly what the WrappedAddress component needs here to force it to pick up the updated address, and at minimal cost. Luckily, each address in our system has an associated ID, so the fix is straightforward:

const WrappedAddress = withAddressHOC(Address)

function FormUsingAddress(props) {
  const { updatedAddress } = props
  // key changes with the address, forcing a remount with the new value
  return (
    <WrappedAddress
      name="propertyAddress"
      key={updatedAddress.id}
      value={updatedAddress}
    />
  )
}

The WrappedAddress type is now stable again (defined once in module scope), and the component only remounts when we actually switch to a different address, rather than on every render.