javascriptreactjsreact-hooks

useState rerenders bindings increment when key added to object but not removed


I'm running a bunch of generated checkboxes through useState as a generic object, eg:

const [stuff, setStuff] = useState(() => {
  // do stuff to get saved stuff.
  // assume very basic format of { uuid: boolean, uuid: boolean, ... }
  return theStuff
}

The stuff is then fed to the UI by child components that uses the stuff, eg:

{stuff.map((thing, i) => (
  <Fragment key={i}>
    <input
      type="checkbox"
      id={thing}
      name={thing}
      checked={stuff[thing] || false}
      onChange={() => handleChanges(thing)}
    />
  </Fragment>
)};

Now when one of the few hundred checkboxes is created and one/many get checked I just throw the state object mentioned in the first snippet. This works great, updates the state on the screen re-renders and also updates the values and re-renders on other sibling components so I can display like counts of selected etc.

export const handleChanges = (thing) => {
  setStuff((prevStuff) => ({
    ...prevStuff,
    [thing]: !prevStuff[thing]
  }));
}

The Problem It may be in handleChanges I assume my learning curve on React lifecycle handling is hurting me because:

Hope this isn't too verbose, but I can't seem to find how to have setState update both when adding and removing from the object on the sibling components. This while keeping the state object a nice clean object of only the uuid's selected.


Solution

  • The issue is that you are using the array index as the React key. When you remove an element from the array all the elements after are shuffled up an index, but the index value is still the same index value, so React bails on rerendering the elements because it doesn't think they changed.

    You should use React keys that are "sticky" to the element/object they represent. Any unique property will suffice. It seems like your data has "uuid" key values, so this is an excellent candidate for React key usage.

    Update the rendering to use the thing value as the mapped React key.

    Here I've used Object.entries to return an array of key-value pairs for mapping, using the uuid key as the React key and identifier, and thing as the checkbox boolean value.

    {Object.entries(stuff).map(([uuid, thing]) => (
      <input
        key={uuid}
        type="checkbox"
        id={uuid}
        name={uuid}
        checked={!!thing}
        onChange={() => handleChanges(uuid)}
      />
    )}