React components that receive ownership of an object as content do not update, how to deal with it?

Asked

Viewed 97 times

0

I basically have a state that is a complex object and several components that render only one property of it, as the application is kind of large I made this prototype to illustrate:

//Component principal que guarda um objeto como state
export default function App() {
  const [data, setData] = useState({
    count: 0,
    title: "generic title"
  });

  const handlerIncrementData = () => {
    const newData = data;
    newData.count = newData.count + 1;
    setData(newData);
  };

  return (
    <div className="App">
      <h2 onClick={handlerIncrementData}>Contador: {data["count"]}</h2>
    </div>
  );
}

Theoretically every time I click on h2 the value of count will be incremented and, in fact, checking by React Dev Tools, it is being, but the rendering of the component does not update. Apparently React does not update components that only use one property of state. Am I doing something wrong or is this part of the library? If so, how can I get around this problem?

The code works perfectly using a primitive type in place of the obj as state.

2 answers

2

These days had the same problem is a question, where I even reported to the user who asked me about copying the new state to the object, which in case I used spread operator, so I understand when will change object(s) or list(s) need to use a new copy of the object and/or list for the component to have this new reality or update information, example:


//Component principal que guarda um objeto como state
function App() {
  const [data, setData] = React.useState({
    count: 0,
    title: "generic title"
  });

  const handlerIncrementData = () => {
    let { count } = data;
    count ++;
    setData({...data, count});    
  };

  return (
    <div className="App">
      <h2 onClick={handlerIncrementData}>Contador: {data["count"]}</h2>
    </div>
  );
}
ReactDOM.render( <App/> , document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

References:

  • 1

    Thank you very much, I’m doing a bit of a big application and it took me a while to realize that some components weren’t working, I was afraid it was too late to change something, anyway, really worth.

1


As stated in documentation of hook useState, React uses the static method Object.is to determine whether two values are equal. The component will only be updated if the unevenness between the values is confirmed.

The comparison made by Object.is is very similar to the operator ===. The difference is that, for Object.is, two values NaN are considered equal and +0 and -0 are considered different.[1]

In this way, we need to remember that, for the === (and therefore to the Object.is also), two objects are equal if they have the same reference. See:

const obj = { name: 'Foo' };
const b = obj; // Mesma referência.

console.log(Object.is(obj, b)); //=> true

// Mesmo se alterarmos em `b` (ou em `obj`), a igualdade manter-se-á:
b.name = 'Bar';

console.log(Object.is(obj, b));

Now see what you’re doing in your component:

const newData = data;
newData.count = newData.count + 1;
setData(newData);

I can see the problem, all right? :)

See what you’re assigning to newData the reference of the object data (the object assignment does not create a "copy", only the reference is assigned). Thus, you do not have a new object, but the same object!

Therefore, even if you modify one of the properties of the object newData (which is the same as data), the algorithm of Object.is still determine that they are the same object of the previous state (since, as we have seen, this comparison is made based on the reference).

Finally, to correct, you must ensure that you are passing a new object (i.e., of different reference) to the function setData.

One way to do this is to use the Operator spread to spread the properties in a new literal (as suggested to another answer):

const handlerIncrementData = () => {
  const { count } = data;
  count++;

  // Note abaixo que um **novo** literal está sendo criado (portanto, diferente referência):
  setData({ ...data, count });    
};

Another option is to use Object.assign, passing a new object in its first argument:

const handlerIncrementData = () => {
  // Novo objeto sendo utilizado como "base":
  //                    ↓↓
  setData(Object.assign({}, data, { count: data.count + 1 }));    
};

In short: for the component to be rendered again, you must ensure that the value you are passing is different from the previous value (according to the comparison of Object.is). In the case of objects, it is sufficient to ensure that the reference is different.

This is precisely why it works if you use a primitive rather than an object. Primitives are compared by value; while objects, by reference.

To learn more about the differences about the Operator spread in objects and Object.assign, read What is the difference between Object.assign and spread Operator?.

  • 1

    perfect answer, thank you very much for the attention

Browser other questions tagged

You are not signed in. Login or sign up in order to post.