[SOLVED] Why doesn’t useState work with deeply nested objects and arrays in them?

Issue

This Content is from Stack Overflow. Question asked by Apoqlite

In my use case I have an array of characters, each character has multiple builds, and each build has a weapons string, and artifacts string. I’m making a tool to select portions of each string and assign them to a value, e.g. assigning index 3-49 of weapons to a specific weapon.

const [characterIndices, setCharacterIndices] = useState<
    { builds: { weaponIndices: SE[]; artifactSetIndices: SE[] }[] }[]
  >([
    ...characters.map((char) => {
      return {
        builds: [
          ...char.builds.map((_build) => {
            return {
              weaponIndices: [],
              artifactSetIndices: [],
            };
          }),
        ],
      };
    }),
  ]);

The SE type is as follows:

type SE = { start: number; end: number; code: string };
//start and end are the respective start and end of selected text
//code is the specific artifact or weapon

The weaponIndices and artifactSetIndices basically hold the start and end of selected text in a readonly textarea.
I have a function to add a SE to either weaponIndices or artifactSetIndices:

const addSE = (
    type: "weaponIndices" | "artifactSetIndices",
    { start, end, code }: SE,
    characterIndex: number,
    buildIndex: number
  ) => {
    let chars = characterIndices;
    chars[characterIndex].builds[buildIndex][type].push({ start, end, code });
    setCharacterIndices((_prev) => chars);
    console.log(characterIndices[characterIndex].builds[buildIndex][type]);
  };

I think that using a console log after using a set function isn’t recommended, but it does show what it’s intended to the weaponIndices, or artifactSetIndices after an entry is added. Passing the addSEfunction alongsidecharacterIndicesto a separate component, and usingaddSE`, does print the respective indices after adding an entry, but the component’s rendering isn’t updated.

It only shows up when I “soft reload” the page, when updating the files during the create-react-app live reload via npm run start.



Solution

React often compares values using Object.is() only to a single level of nesting (the tested object and its children).

It will not re-render if the parent is found equal, or if all the children are found equal.

React then considers that nothing has changed.

In your implementation, even the first top-level check will immediately fail, since Object.is(before, after) will return true.

You could use an Immutable objects approach to eliminate this concern when setting a new state (either directly through spreading values or with a support library such as Immer).

For example instead of setting the values within the object…

myObj.key = newChildObj

…you would make a new object, which preserves many of the previous values.

myObj === {...myObj, key: newChildObj}

This means that every changed object tree is actually a different object (with only the bits that haven’t changed being preserved).

To read more about this see https://javascript.plainenglish.io/the-effect-of-shallow-equality-in-react-85ae0287960c


This Question was asked in StackOverflow by Apoqlite and Answered by cefn It is licensed under the terms of CC BY-SA 2.5. - CC BY-SA 3.0. - CC BY-SA 4.0.

people found this article helpful. What about you?