5

I recently read the react.js documentation and found inconsistencies in setting the state based on previous state value. Here is that chunk of code:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

I thought this way () => this.setState({ count: this.state.count + 1 }) of setting state is wrong and you should use callback for that purpose instead. So I've raised the PR in which I add use of callback function with previous state and it was deprecated because

The callback version is useful when you're not certain what the captured value of state is.

I don't really like when you're not certain part of the explanation and can someone explain why do this way () => this.setState({ count: this.state.count + 1 }) of setting the state is correct.

Thanks in advance.

Kevin Hernandez
  • 1,270
  • 2
  • 19
  • 41
  • In this example you aren't really at risk of getting a stale copy of state. You only call `setState` once, and the event will only happen once per render. So using the callback doesn't seem to be required. – Brian Thompson Mar 09 '20 at 15:14
  • 1
    I've asked for clarification on the PR. That is the place for this conversation. – Dave Newton Mar 09 '20 at 15:14
  • @BrianThompson I'm not convinced that advocating against React doc's [own guidelines](https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous) in their own examples is a good idea, though. – Dave Newton Mar 09 '20 at 15:16
  • Пожалуйста - IMO you're right and the example should be updated to be consistent . – Dave Newton Mar 09 '20 at 15:23
  • @DaveNewton in my opinion the example from the document is correct in the context that it was used. – goto Mar 09 '20 at 16:58
  • @goto1 It's impossible to determine if the example is correct in context since `this.props` is also accessed directly. There's a reason [the docs](https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous) call out this scenarios and explicitly recommend using the callback when computing state based on current state and props: "Because `this.props` and `this.state` may be updated asynchronously, you should not rely on their values for calculating the next state." While I might not bother with the PR, at the very least their docs should be consistent. – Dave Newton Mar 09 '20 at 17:10
  • 1
    @DaveNewton the argument could go both ways, but for the example they're using it is definitely correct. My guess is that they don't want to start throwing more advances concepts immediately and just show a simple use case. Using the `callback function` with `prev/curr state` wouldn't really yield any benefits or improve the code - https://github.com/reactjs/reactjs.org/blob/master/content/docs/hooks-state.md – goto Mar 09 '20 at 17:16
  • @ goto, So, if we are paranoid or suspicious, can we still use the callback form? –  Jul 02 '22 at 14:57

2 Answers2

1

In your current example, where you set your default state to { count: 0 }, you are "safe" to do setState({ count: this.state.count + 1 }) because when you first update your state, 0 + 1 will produce a valid result.

class App extends React.Component {
  state = { count: 0 }
  render() {
    return (
      <div>
         <p>You clicked {this.state.count} times</p>
         <button
           onClick={() => this.setState({ count: this.state.count + 1 })}
         >
           Click me!
         </button>
      </div>
    )
  }
}
ReactDOM.render(
  <App />,
  document.getElementById("app")
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>

However, let's assume your initial value for some piece of state is not 0, so calling this.state.count + 1 could produce an invalid result. This is where you'd reach for the callback version because:

you're not certain what the captured value of state is.

class App extends React.Component {
  state = { count: null }
  render() {
    const handleClick = () => {
      this.setState((prevState) => {
        // count is initially `null`, so 
        // `null + 1` could yield an undesired result
        if (!Number.isInteger(prevState.count)) {
          return { count: 1 }
        }
        return { count: prevState.count + 1 }
      })
    }
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={handleClick}>
          Click me!
        </button>
      </div>
    ) 
  }
}
ReactDOM.render(
  <App />,
  document.getElementById("app")
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>

This is just an example, but you get the idea.


Your PR was most likely declined because the example in the docs is correct, assuming that is used in the same context where it is "safe" to update your state by doing this.setState({ count: this.state.count + 1 }).

Here's the documentation:

Both ways of updating state are correct and should be used when appropriate. As you can see in the second example, the "callback option" would be a better solution if want to do some checks prior to updating your state.

Still, the example in the documentation is correct and wouldn't produce any benefits if it was using the "callback option" instead.

goto
  • 4,336
  • 15
  • 20
  • By the way, then I also can do check like this one: ```javascript – Ярослав Терещук Mar 09 '20 at 18:20
  • @ЯрославТерещук Yeah I guess you could, but that looks messy. Use whatever approach you think is appropriate for what you're doing, but the above should give you a good idea why your PR was rejected. – goto Mar 09 '20 at 18:22
  • ok, so I've got your point, but where can I read more about this? For example when I will get a stale state? – Ярослав Терещук Mar 09 '20 at 18:35
  • @ЯрославТерещук depends on you're doing & how complicated the component is. I'd start with https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous & https://reactjs.org/docs/react-component.html#setstate, but it'll depend on how `React` triggers updates. If you're trying to access `this.state.count` inside of `render`, you probably wouldn't be getting "stale" state, but if you had a method on your component that was trying to use `this.state.count` to update the value of `state.count`, you might run into issues, so use callback. – goto Mar 09 '20 at 18:47
  • thanks, but I don't think that official react documentation will help me with that because they do not describe that kind of situation and omit this in the place where I'm trying to add the changes in my PR. And I'm familiar with this material. – Ярослав Терещук Mar 09 '20 at 18:58
  • @ЯрославТерещук Again, you will not get "stale" `state` in the `render` method, and this is what the documentation you're referring to is using, hence it doesn't need the callback style as you're suggesting. If you had a method that was sitting outside of `render` and using `this.state.count` to update `state.count`, that's where you could potentially run into issues. – goto Mar 09 '20 at 19:03
  • 1
    @ЯрославТерещук I will update those examples if I can come up with a way to break this and show what I am talking about. – goto Mar 09 '20 at 19:09
0

Use the return function

By passing the function here you will always have access to the previous state.

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

Note you can also add props as a second arg.

If you would like to read more about this callback check out this article. It goes into great detail.

Joe Lloyd
  • 19,471
  • 7
  • 53
  • 81