8

Considering simple state hook:

const [count, setCount] = React.useState(0);

I want to increase or decrease the count. Basically do the same thing shown in the hooks docs.

But as a known fact for the old this.setState function:

Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.

and the right way to update the state based on the old state is:

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

Does the same thing applies to setCount? Or I can be sure that count is always up-to-date?

n1stre
  • 5,856
  • 4
  • 20
  • 41

2 Answers2

9

useState hooks work differently than this.setState. Calling your setter, setCount in the below example, does work asynchronously, but since count is not changed during the rendering of a functional component the evaluation is deterministic.

The following example, copied verbatim from Hooks at a Glance in the React docs, is 100% safe (you won't have any errors about calling methods on unmounted components) and will behave as expected:

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

The only time you would need to use the functional update where you pass a function to setCount is if you need to call setCount multiple times when referencing state. Note that this isn't because setState is asynchronous—it's because count is not updated until the next render.

For example, this would be a problem:

<button
  onClick={() => {
    setCount(count + 1)
    setCount(count + 1) // since `count` is not dynamically updated mid-render, this evaluates to the same value as above
  }}
/>

In practice, this pattern is relatively rare. You wouldn't call the setter multiple times in a single function in this way. Instead, if you are passing the setter as a prop on children, and those children reference the state controlled by the setter, then you would use the functional update pattern. Even in this scenario, though, React will re-render the component with each successive value.

As for concerns about whether this is a reliable and/or recommended approach, the React documentation has this to say about the matter (source):

You might hear a recommendation to always write code like setCount(c => c + 1) if the state you’re setting is calculated from the previous state. There is no harm in it, but it is also not always necessary.

In most cases, there is no difference between these two approaches. React always makes sure that for intentional user actions, like clicks, the count state variable would be updated before the next click. This means there is no risk of a click handler seeing a “stale” count at the beginning of the event handler.

However, if you do multiple updates within the same event, updaters can be helpful. They’re also helpful if accessing the state variable itself is inconvenient (you might run into this when optimizing re-renders).

If you prefer consistency over slightly more verbose syntax, it’s reasonable to always write an updater if the state you’re setting is calculated from the previous state. If it’s calculated from the previous state of some other state variable, you might want to combine them into one object and use a reducer.

coreyward
  • 77,547
  • 20
  • 137
  • 166
  • 3
    except when onClick handler is async and has some logic before setCount – Max Jan 20 '20 at 17:10
  • 3
    @coreyward does it look like there's specific onClick example provided in the question? The question is about setCount – Max Jan 20 '20 at 17:12
  • @SaveliTomac The example I provide will never result in referencing an unmounted component because the setter (`setCount`) is stable across renders. The only effective difference between the callback method and the direct value method used here is that if the `onClick` handler is fired more than once before the next render the count only increases/decreases by 1. This is often what you would expect anyways—queuing UI-tied actions when thread locking occurs creates a waterfall of unexpected events. – coreyward Jan 20 '20 at 17:37
  • Why do you say about unmounted component? I try to explain you that your example can skip increment of counter – svltmccc Jan 20 '20 at 17:39
  • @SaveliTomac I assure you this is not the case. `count` is always the most up-to-date value when the render occurs, and since `useState` hooks cannot dynamically update the value of `state` mid-render, the `count` value here is stable. – coreyward Jan 20 '20 at 17:42
  • @coreyward read this part of documentation, please https://reactjs.org/docs/hooks-reference.html#functional-updates – svltmccc Jan 20 '20 at 17:44
  • 4
    guys, chill, toxicity leads nowhere. @coreyward thanks for your answer – n1stre Jan 20 '20 at 17:55
  • @coreyward let's break this down. Docs specifically suggest that in this case (when next value depends on the previous one) one should use functional update and not direct update. You contradict it, claiming you know better and it never bugs. I can understand it as docs can often be overly cautious about suggesting things. But I expect you to prove your point as well. So far your explanations haven't been working out: this has nothing to do with unmounting components and identity of setCount – Max Jan 20 '20 at 17:58
  • furthermore, the closure you mention is exact source of a problem. Are you sure you tried all the scenarios to ensure your real world apps perform as expected? What if it is not an onClick but any other handler that may or may not run another logic before executing provided function? What if memiozation is used? What if there's heavy re-rendering going on along with programmatic updates of the value? – Max Jan 20 '20 at 17:59
  • @Max Due to the way `useState` works all of these are fine. `count` is not a reference, and `setState` is stable across renders per the documentation. The comment about unmounting is specifically in reference to common issues with the use of synchronous `this.x` values in callbacks. Memoization via `useMemo` is no different than normal; memoization via `React.memo` doesn't affect hook-based rerenders. Re: “programmatic updates” — `setCount` doesn't alter the local copy of `count`; it queues a subsequent render—is there a specific concern this doesn't address? – coreyward Jan 20 '20 at 18:32
3

I am not sure the exact logic, and the use case of the count state, but generally, you will need to call

setCount(count + 1); 

Or, functional updates using callbacks

setCount(prev => prev + 1); 

If you wish to update your state. However, do take note that the updating of state can be asynchronous, which is similar to the way class component's setState() works. You may refer to the useState documentation for more details.

wentjun
  • 40,384
  • 10
  • 95
  • 107