React: If the state is an array of object, it seems cannot listen the state updating when I modify any object

Created on 11 Jun 2019  路  11Comments  路  Source: facebook/react

If the state is an array of object, it seems cannot listen the state updating when I modify any object

It may be my fault, but the issue seems exist.

Version

16.8.6

Related

React useState Hook don't rerender the changes #15595

Situation

  1. I used useState to create a state which is an array of object.
  2. I used the either function below to update state:
    jsx const onChangeData = (key, value, id) => { state.forEach((a, i) => { if (a.id === id) { state.splice(i, 1, { ...a, [key]: value }); } }); setState(state); };
    jsx const onChangeData = (key, value, id) => { const temp = data; temp.forEach((a, i) => { if (a.id === id) { temp.splice(i, 1, { ...a, [key]: value }); } }); setState(temp); };
  3. I used useEffect to listen the state updating and do something, but it doesn't work:
    jsx useEffect(()=>{ // do something }, [state]);
  4. I modified the function above, and it works:
    jsx const onChangeData = (key, value, id) => { let temp = []; temp = temp.concat(state); temp.forEach((a, i) => { if (a.id === id) { temp.splice(i, 1, { ...a, [key]: value }); } }); setState(temp); };

Possible reason

So I guess React checks the first layer of the array state only (own properties? ), or checks the memory stack of the state only, no deeper comparing. (Just my guess)

Expect

I am not sure the reason of the issue, maybe it is my fault, maybe it is a solution for the performance. I hope that someone can tell me the real reason, and how to use it exactly.

At last

Thanks in advance for anybody who answers me!

Most helpful comment

And when you work with react states then don't use methods which modify things (like objects, arrays etc) in place. In your example splice method modifies state in place. Instead I would write it like this:

const onChangeData = (key, value, id) => {
  const nextState = state.map(a => a.id === id ? { ...a, [key]: value } : a);
  setState(nextState);
 };

All 11 comments

Your first approach doesn't work because of this https://reactjs.org/docs/hooks-reference.html#bailing-out-of-a-state-update.

If we look at the code you have written:

const onChangeData = (key, value, id) => {
  state.forEach((a, i) => {
    if (a.id === id) {
       state.splice(i, 1, { ...a, [key]: value });
     }
  });
  setState(state);
 };

and the fact that useState tries bailing out the updates when the state is the same then we will see that your setState calling receives the same array reference.

And when you work with react states then don't use methods which modify things (like objects, arrays etc) in place. In your example splice method modifies state in place. Instead I would write it like this:

const onChangeData = (key, value, id) => {
  const nextState = state.map(a => a.id === id ? { ...a, [key]: value } : a);
  setState(nextState);
 };

@dmytro-lymarenko I think I understand the array.map way you showed.

@dmytro-lymarenko So according to the introduction which you sent me, React hooks use Object.is to compare the old state and the current one. For this reason, it won't to check deeper. Ok, got it. Thanks for your help, thanks a lot. 馃

@JandenMa yes, so, in order to be sure that always perform the rendering, you need to be sure to pass to the hook a new instance, in my case I found practical use spread operator for that: setStateMyList([..stateMyList]). (of course this make sense if you use method mutators against stateMyList instead of accessor method to override it, so change their instance).

@Jero786 Ya, I am with you on that! Thx!

If you want your hook to detect state change on an array, you need to copt the previous array,
try using Array.from() when getting your array and then set the new array state value

Object.is doesn't seem to look deep enough. I ran into a problem when updating parent state via a child two layers deep.

For example, say you have an object Dog that has a property ears of type strings.

Object.is says that the dog and nextDog objects are equal, despite the fact that nextDog is modified.

// parent
const [dog, setDog] = useState<Dog>(DefaultDog);

// child's child
const nextDog = dog;

nextDog.ears = [...nextDog.ears, "another ear"];
changeDogFromChildsChild(nextDog);

The only solution is to do something weird, such as using const nextDog = Object.assign({}, dog) or JSON.stringify immediately followed by a JSON.parse. This really seems quite dumb really.

Not sure if there's really a good way around it though.

@JandenMa I know this issue is since closed, but I would like to add my input as I came across similar issue. Taking from @dmytro-lymarenko's response, useState() would like to keep things immutable and its good practice to always create a copy of the array/object, currently "watched" by useState:

const onChangeData = (key, value, id) => {
  let nextState = [...state]
  nextState.key = 'someNewKey' // you can set whatever you desire 
  setState(nextState);
 };

@tochidlife For future Googlers, I think you meant nextState[key] = value in this particular case :)

And when you work with react states then don't use methods which modify things (like objects, arrays etc) in place. In your example splice method modifies state in place. Instead I would write it like this:

const onChangeData = (key, value, id) => {
  const nextState = state.map(a => a.id === id ? { ...a, [key]: value } : a);
  setState(nextState);
 };

Thank you @dmytro-lymarenko :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

varghesep picture varghesep  路  3Comments

zpao picture zpao  路  3Comments

trusktr picture trusktr  路  3Comments

bloodyowl picture bloodyowl  路  3Comments

zpao picture zpao  路  3Comments