Home > Back-end >  Why doesn't useState work with deeply nested objects and arrays in them?
Why doesn't useState work with deeply nested objects and arrays in them?

Time:09-18

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 addSE function alongside characterIndices to a separate component, and using addSE, 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.

In case you are confused about what the data types are, I've made a github repo, at https://github.com/ChrisMGeo/ght-indexer/tree/main/src at src/data.json. That JSON file describes what the character data looks like, including the builds, and each build's weapons and artifacts(called artifact_sets in the JSON)

CodePudding user response:

Looks to me you are not updating the state at all. Here you are just storing the same object reference that you already have in state into a new variable chars.

let chars = characterIndices;

chars now holds reference to a same object as characterIndices.

Here you are mutating that same object

chars[characterIndex].builds[buildIndex][type].push({ start, end, code });

And here you are updating the state to the same object that is already in the state. Notice that no state update here occurs.

setCharacterIndices((_prev) => chars);

Object you have in state is mutated, but you did not "change" the value of the state, thus no component re-render.

What you could maybe do is create a copy of the object, mutate that and update the state. just change chars assignment like this:

 let chars = {...characterIndices};

CodePudding user response:

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

  • Related