简体   繁体   中英

React JS setState behaviour

I was following this tutorial , And on 1:10:18 (timestep included in link) he wrote this.setState({ count: this.state.count + 1 }); (where count is a primitive value inside state object, that is then displayed on page after button click) From tutors words i understood that we need to specify count: inside setState so that React will update this value in state object. But i noticed that i can just write some bullshit in setState like:

  handleIncrement = key => {
    this.state.totalCount += 1;
    this.state.tags[key] += 1;
    this.setState({ countBlabla: 23213123 });
  };

And HTML still will be updated correctly, without re-rendering everything. (I've checked in developer tools, only values that were changed are updated, everything else is untouched)

I have also tried: this.setState({}); works good. But this.setState(); does not.

So what's the point of passing setState a parameter?

Only place you can directly write to this.state should be the Components constructor. In all the other places you should be using this.setState function, which will accept an Object that will be eventually merged into Components current state.

While it is technically possible to alter state by writing to this.state directly, it will not lead to the Component re-rendering with new data, and generally lead to state inconsistency.

Also, this.setState() is async function so you won't see the changes straight away if you just try to console.log() the new state after it is called. Instead, you can pass a callback function this.setState({var: newVar}, () => {console.log(this.state)}) .

Following this line of code of react package:

setState method just only call the enqueueSetState method. Which in his turn enqueue updating of particular react component. Update is processing by react-dom package (or by react-native )

If you will take a look on forceUpdate method, you will see that its doing actually the same thing (calling enqueueSetState ). And this method is only triggering reconciliation process.

So in short it is not necessary mutate state or not from the pure triggering rerender prospective. But it is highly necessary from the optimization and clear data-flow prospective. If you will rely on the state reference when using setState you can meet tricky behavior when some components will see old state.

You need to pass only that values that needs to be updated. React will make spread of current state into new one. If your state now is { count: 1, prop: 2 } :

// if you will make 
this.setState({ count: 11 })

// react will do
{ ...currentState, count: 11 }

// and result will be
{ prop: 2, count: 11 }

And enque updating of component by calling reconciliation process. That's it and don't forget to:

Just don't mutate state :)

PS Here is nice article by Dan Abramov (core member of react team)

In general, you never want to directly mutate the state. Always use setState .

The answer to your question, why do we need to specify the key ( count in your example): Because that's what we want to change. setState doesn't just re-render the component and we don't specify a key just for the purpose of re-rendering. It changes state and then re-renders the component so it could use the new values in state.

While technically you can mutate state and call either forceUpdate or setState with random parameters or an object, you shouldn't do that because that breaks the natural workflow of React component design.

If you pass an object to setState, it internally merges it and causes a re-render and if nothing is passed it skips the re-render which is the behaviour that you observe

Now lets look at the downside of the above code

handleIncrement = key => {
    this.state.totalCount += 1;
    this.state.tags[key] += 1;
    this.setState({ countBlabla: 23213123 });
  };

In the above case, let suppose you pass totalCount and tags as props to the child component and in the child component based on totalCount change or tags change you want to make an API call. According to React flow, you would do that in componentDidUpdate lifecycle method

componentDidUpdate(prevProps) {
    if (prevProps.totalCount !== this.props.totalCount) {
        // make api cal;
    }
}

However, although the above code will work if totalCount is interger, boolean or string since they are immutable, it won't work with an object. such cases will be difficult to debug and might lead to unexpected behaviours.

What you are doing is called programming by coincidence.

Your code just happens to work due to the internal workings of react it is however undocumented behaviour and might stop working in the future without warning.

The suggested way is to use this.setState({...}) and pass an object of all changes made to the state. In your case you would do:

  this.setState({ 
      totalCount: this.state.totalCount + 1 
      tags: {
           ...this.state.tags,
          [key]: this.state.tags[key] + 1 
       }  
  });

This will update the state and achieve the same side-effects as your code.

However it is important to treat the current state as immutable because for example if at some point in the future you decide to implement a shouldComponentUpdate method and reject a state change if the state was mutated manually you will have some unintended consequences due to an inconsistent previous state to what you'd expect.

In addition to other answers, if you are going to access the previous state, you can also pass it as a param to the setState.

Eg:

 this.setState((prevState) => ({
      totalCount: prevState.totalCount + 1 
      tags: {
           ...prevState.tags,
          [key]: prevState.tags[key] + 1 
       }  
  }));

This is recommended as this.state may be updated asynchronously.

More info: https://reactjs.org/docs/state-and-lifecycle.html

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM