最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Is it okay to treat the prevState argument of setState's function as mutable? - Stack Overflow

programmeradmin3浏览0评论

I know this.state is not supposed to be modified directly, instead setState should be used.

From this I inferred that prevState should be also treated as immutable and instead setState should always look something like this:

this.setState((prevState) => {
  // Create a new object instance to return
  const state = { ...prevState };
  state.counter = state.counter + 1;
  return state;
});

Or with deeper nesting:

this.setState((prevState) => {
  // Create a new object instance to return
  const state = { ...prevState, counter: { ...prevState.counter } };
  state.counter.value = state.counter.value + 1;
  return state;
});

Or just a partial update like would be with setState({}) where easier and nicer to use:

this.setState((prevState) => ({ counter: prevState.counter + 1 }));

All of the above are obviously correct because they return a new instance, but then I came across this question where the accepted answer encourages mutating prevState without returning a new object instance (notice the code block in the question).

Something like this:

this.setState((prevState) => {
  prevState.flag = !prevState.flag;
  return prevState;
});

I found this to be a sketchy suggestion so I decided to test if the object instance references of prevState and this.state are the same:

(The JSFiddle)

class Test extends React.Component {
  state = { counter: 0 };

  onDemonstrateButtonClick = (event) => {
    this.setState((prevState) => {
      if (prevState === this.state) alert(`uh, yep`);
      prevState.counter++;
      return prevState;
    });
  };

  render() {
    return (
      <div>
        <button onClick={this.onDemonstrateButtonClick}>Demonstrate</button>
        {this.state.counter}
      </div>
    );
  }
}

Whadayaknow, they are! So which is it? Is the answer wrong and should I return a new object instance or return partial update as a new object or can I go wild and mutate the prevState argument directly? And if yes, how is this any different from mutating this.state directly?

Side note: TypeScript React typings do not mark the argument as ReadOnly which only adds to my confusion.

I know this.state is not supposed to be modified directly, instead setState should be used.

From this I inferred that prevState should be also treated as immutable and instead setState should always look something like this:

this.setState((prevState) => {
  // Create a new object instance to return
  const state = { ...prevState };
  state.counter = state.counter + 1;
  return state;
});

Or with deeper nesting:

this.setState((prevState) => {
  // Create a new object instance to return
  const state = { ...prevState, counter: { ...prevState.counter } };
  state.counter.value = state.counter.value + 1;
  return state;
});

Or just a partial update like would be with setState({}) where easier and nicer to use:

this.setState((prevState) => ({ counter: prevState.counter + 1 }));

All of the above are obviously correct because they return a new instance, but then I came across this question where the accepted answer encourages mutating prevState without returning a new object instance (notice the code block in the question).

Something like this:

this.setState((prevState) => {
  prevState.flag = !prevState.flag;
  return prevState;
});

I found this to be a sketchy suggestion so I decided to test if the object instance references of prevState and this.state are the same:

(The JSFiddle)

class Test extends React.Component {
  state = { counter: 0 };

  onDemonstrateButtonClick = (event) => {
    this.setState((prevState) => {
      if (prevState === this.state) alert(`uh, yep`);
      prevState.counter++;
      return prevState;
    });
  };

  render() {
    return (
      <div>
        <button onClick={this.onDemonstrateButtonClick}>Demonstrate</button>
        {this.state.counter}
      </div>
    );
  }
}

Whadayaknow, they are! So which is it? Is the answer wrong and should I return a new object instance or return partial update as a new object or can I go wild and mutate the prevState argument directly? And if yes, how is this any different from mutating this.state directly?

Side note: TypeScript React typings do not mark the argument as ReadOnly which only adds to my confusion.

Share Improve this question edited Nov 16, 2017 at 21:59 Tomáš Hübelbauer asked Nov 16, 2017 at 21:47 Tomáš HübelbauerTomáš Hübelbauer 10.7k17 gold badges74 silver badges141 bronze badges 7
  • It's the first time I hear that you have to clone whole state object before using setState function. It's kind of abstraction for me. Anyways - you can't mutate but their properties - e.g. if you have an array, you can't use push method because it works in situ and will mutate. That's why it's suggested to use spread syntax or even concat. The same with integers. i++ will mutate, but i + 1 nope. – kind user Commented Nov 16, 2017 at 21:52
  • Short answer: don't mutate prevState either. – Alex Guerra Commented Nov 16, 2017 at 21:55
  • 1 @TomášHübelbauer Summing up - this.setState((prevState) => ({ counter: prevState.counter + 1 })); is fully correct for me. But if anyone more experienced than me could approve it or reject, would appreciate it. – kind user Commented Nov 16, 2017 at 21:58
  • 1 The statement "state should be immutable", is often repeated without reason. State must be immutable if using PureComponent because React will do a shallow parison checking for different object references. If paring state in shouldComponentUpdate, then immutability might help for efficient reference equality checks. Otherwise, feel free to mutate state since React will blindly rerender without checks anyway. – Rick Jolly Commented Nov 16, 2017 at 22:16
  • 1 Don't mutate. The answer you link states it is going to work, not that it should be done. Notice how he write about a "clearer" alternative right after. – zeh Commented Nov 17, 2017 at 0:29
 |  Show 2 more ments

1 Answer 1

Reset to default 13

First Point

Is it okay to treat the prevState argument of setState's function as mutable?

The answer is NO you should never mutate prevState, this is also clearly mentioned in react documentation in setState section

  • prevState is a reference to the previous state. It should not be directly mutated. Instead, changes should be represented by building a new object based on the input from prevState and props.

Second Point:

You tested prevState and this.state and they are the same, well actually they are not.

To figure out why they are actually different we need to know why prevState actually exist, and the answer is that setState function is asynchronous, thats why react js is giving us access to prevState lets check the example below where prevState != this.state

In the example below we will increment counter twice per click, but we will use 2 setState operations each one of them will increment the counter by 1.

Because setState is async you will notice that the second setState operation started before the first setState is finished this is where prevState is useful and here prevState and this.state are not equal.

I mented each line with a number denoting when this line is executed, this should explain why we need prevState and why it is different from this.state.

class App extends React.Component{
  constructor(props)
  {
    super(props);
    this.state = {
      counter : 1
    };
  }

  increment = () =>{
    this.setState((prevState , props) => {
      console.log("in first"); //3
      console.log(prevState);  //3
      console.log(this.state); //3
      if(prevState == this.state)
      {
         console.log("in first prevState == this.state");
      }
      return {
        counter : prevState.counter+1
      }
    } , ()=>{
      console.log("done updating first"); //5
    });


    console.log("after first"); //1

    this.setState((prevState, props) => {
      console.log("in second"); //4
      console.log(this.state);  //4
      console.log(prevState);   //4
      if (prevState == this.state) {
        console.log("in second prevState == this.state");
      }
      return {
        counter: prevState.counter + 1
      }
    } , () =>{
      console.log("done updating second"); //6
    });

    console.log("after second"); //2

  }

  render(){
    return (
      <div>
        <span>{this.state.counter}</span>
        <br/>
        <button onClick={this.increment} >increment</button>
      </div>
    )
  }
}

The Result from the above code is

"after first"
"after second"
"in first"
▶Object {counter: 1}
▶Object {counter: 1}
"in first prevState == this.state"
"in second"
▶Object {counter: 1}
▶Object {counter: 2}
"done updating first"
"done updating second"

The above code is fully working in this link, you can check the console.log result https://codesandbox.io/s/k325l485mr


The above example will correctly increment counter twice per click, if you want to break it change return statement in second setState

from

return {
    counter: prevState.counter + 1
}

to

return {
    counter: this.state.counter + 1
}

and you will find that the result is not correct each click will result in 1 increment which is not correct because we have 2 setState , this is because we didn't use prevState and we used an incorrect this.state

Finally

I believe that the correct way to update the counter is

this.setState((prevState) => ({ counter: prevState.counter + 1 }));

发布评论

评论列表(0)

  1. 暂无评论