Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React - Changing the state without using setState: Must avoid it?

My code works, but I have a best practice question: I have an array of objects in the state, and a user interaction will change a value of one object at a time. As far as I know, I'm not supposed to change the state directly, i should always use setState instead. If I want to avoid that with any price, I will deep clone the array by iteration, and change the clone. Then set the state to the clone. In my opinion avoiding to change the state that I will change later anyway is just decreasing my performance.

Detailed version: this.state.data is an array of objects. It represents a list of topics in a forum, and a Favorite button will toggle, calling clickCollect(). Since I have an array in the state, when I change the is_collected property of one item, I need to create a copy of the array to work with, and after changing to the new value, I can set it to the state.

var data = this.state.data.slice(0);
data[index].is_collected = !data[index].is_collected;
this.setState({data: data});

var data = this.state.data : This would copy the pointer to the array and push(), shift(), etc would alter the state directly. Both data and this.state.data will be affected.

var data = this.state.data.slice(0) : This makes a shallow clone, push and shift doesn't change the state but in my clone I still have pointers to the elements of the state's array. So if I change data[0].is_collected, this.state.data[0].is_collected gets changed as well. This happens before I call setState().

Normally I should do:

var data = []; 
for (var i in this.state.data) {
    data.push(this.state.data[i]); 
}

Then I change the value at index, setting it to true when it's false or false when it's true:

data[index].is_collected = !data[index].is_collected;

And change state:

this.setState({data: data});

Consider my array is relatively big or enormously big, I guess this iteration will reduce the performance of my APP. I would pay that cost if I knew that it is the right way for any reason. However, in this function (clickCollect) I always set the new value to the state, I'm not waiting for a false API response that would say to stop making the change. In all cases, the new value will get into the state. Practically I call setState only for the UI to render again. So the questions are:

  1. Do I have to create the deep clone in this case? (for var i in ...)
  2. If not, does it make sense to make a shallow clone (.slice(0)) if my array contains objects? The changes are being made on the objects inside of the array, so the shallow clone still changes my state, just like a copy (data = this.state.data) would do.

My code is simplified and API calls are cut out for simplicity.

This is a beginner's question, so a totally different approach is also welcome. Or links to other Q & A.

import React from 'react';

var ForumList = React.createClass({
  render: function() {
      return <div className="section-inner">
        {this.state.data.map(this.eachBox)}
      </div>
  },
  eachBox: function(box, i) {
    return <div key={i} className="box-door">
        <div className={"favorite " + (box.is_collected ? "on" : "off")} onTouchStart={this.clickCollect.bind(null, i)}>
          {box.id}
        </div>
    </div>
  },
  getInitialState: function() {
    return {data: [
      {
        id: 47,
        is_collected: false
      },
      {
        id: 23,
        is_collected: false
      },
      {
        id: 5,
        is_collected: true
      }
    ]};
  },
  clickCollect: function(index) {
    var data = this.state.data.slice(0);
    data[index].is_collected = !data[index].is_collected;
    this.setState({data: data});
  }
});

module.exports = ForumList;
like image 493
Szalai Laci Avatar asked Feb 06 '17 05:02

Szalai Laci


3 Answers

Personally I don't always follow the rule, if you really understand what you are trying to do then I don't think it's a problem.

var data = this.state.data.slice(0);
data[index].is_collected = !data[index].is_collected;
this.setState({data: data});

In this case, mutating state and calling the setState again like this is fine

this.state.data[index].is_collected = !this.state.data[index].is_collected;
this.setState({data: this.state.data});

The reason you should avoid mutating your state is that if you have a reference to this.state.data, and calling setState multiple times, you may lose your data:

const myData = this.state.data
myData[0] = 'foo'
this.setState({ data: myData })
// do something...
// ...
const someNewData = someFunc()
this.setState({ data: someNewData })

myData[1] = 'bar' // myData is still referencing to the old state
this.setState({ data: myData }) // you lose everything of `someNewData`

If you really concerned about this, just go for immutable.js

like image 153
CodinCat Avatar answered Nov 08 '22 10:11

CodinCat


Muting the state directly breaks the primary principle of React's data flow (which is made to be unidirectional), making your app very fragile and basically ignoring the whole component lifecycle.

So, while nothing really stops you from mutating the component state without setState({}), you would have to avoid that at all costs if you want to really take advantage of React, otherwise you would be leapfrogging one of the library's core functionalities.

like image 2
Kelvin De Moya Avatar answered Nov 08 '22 12:11

Kelvin De Moya


If you want follow react best practices, you should do shallow copy of all your array, when you change any property. Please look into "immutable" library implementation.

But, from my experience, and from my opinion, setState method should be called if you have "shouldCompomenentUpdate" implementations. If you think, that your shallow copy will be consume much more resources, then react virtual dom checks, you can do this:

this.state.data[0].property = !this.state.data[0].property;
this.forceUpdate();
like image 1
degr Avatar answered Nov 08 '22 10:11

degr