8

I've just started learning React and got to know about the useState hook. I came across two different ways for setting the state for boolean data. So are these two approaches identical and if not, which one is one should prefer?

const [isChanged, setIsChanged] = useState<boolean>(false)
  
const onClick = () => {
    setIsChanged((prevState) => !prevState)  // Approach 1
    setIsChanged(!isChanged)  // Approach 2
}
Swanand
  • 97
  • 1
  • 15
  • 6
    the first one is safer, because state is async the 2nd approach might be buggy in some cases. the first state makes sure it is the opposite of the previous one. – Michael Jan 03 '22 at 12:51
  • 1
    Use the 1st case as if you are using the useState hook and also because your new state depends on the previous state. – Hamza Khan Jan 03 '22 at 12:53
  • The docs explain this pretty well https://reactjs.org/docs/faq-state.html – Alex Wayne Jan 05 '22 at 18:04
  • 2
    @AlexWayne I don't think that link is particularly useful - it appears to just be talking about the `setState` method in class components, rather than hooks. Even if they use much the same underlying mechanism, the question here is specific to hooks because there is no equivalent to "approach 1" for setting state in class components. – Robin Zigmond Jan 05 '22 at 18:56
  • @RobinZigmond Uh, of course there is an equivalent to #1 for updating state in class components, it's just a functional state update, i.e. `this.setState(prevState => {.... })`. I do agree there are other more informational React docs. This is such a trivial question, not sure why OP felt the need to bounty it, or why it hasn't been close voted as opinionated (*the preference part of the question, it's subjective*). Any time the next state ***necessarily depends*** on the previous state, it's common practice to use the functional state update (approach 1). – Drew Reese Jan 06 '22 at 06:41
  • 1
    [State updates](https://reactjs.org/docs/react-component.html#setstate) and more specifically, [functional updates](https://reactjs.org/docs/hooks-reference.html#functional-updates) for the `useState` hook, explain well enough the reason approach 1 would be preferred over plain state updates. – Drew Reese Jan 06 '22 at 06:52
  • @DrewReese thanks, not sure how I forgot about that! – Robin Zigmond Jan 06 '22 at 07:20

6 Answers6

11

Since, as often in code, a simple example paints a thousand words, here's a simple CodeSandbox demo to illustrate the difference, and why, if you want an update based on the value of the state at the point of update, the "updater function" (Approach 1) is best:

https://codesandbox.io/s/stack-overflow-demo-nmjiy?file=/src/App.js

And here's the code in a self-contained snippet:

<div id="root"></div><script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/@babel/standalone@7.16.7/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">

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

  // this uses the "good way" but it doesn't really matter here
  const incrementPlain = () => setCount((oldCount) => oldCount + 1);

  const incrementWithTimeoutBad = () =>
    setTimeout(() => setCount(count + 1), 3000);
  const incrementWithTimeoutGood = () =>
    setTimeout(() => setCount((oldCount) => oldCount + 1), 3000);

  return (
    <div>
      <div>Current count: {count}</div>
      <div>
        <button onClick={incrementPlain}>
          Increment (doesn't matter which way)
        </button>
      </div>
      <div>
        <button onClick={incrementWithTimeoutBad}>
          Increment with delay (bugged)
        </button>
        <button onClick={incrementWithTimeoutGood}>
          Increment with delay (good)
        </button>
      </div>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));

</script>

Here we have a simple numeric "count" state which is displayed in the markup, together with 3 different buttons that all increment it.

The one on top just does a direct increment - I happen to have used the function form ("approach 1") here because I prefer this style for reasons that will hopefully become clear, but as my comment says, it doesn't actually matter here.

The two below use the two different approaches you outline in the question, and do so after a delay. I've done this with setTimeout here just to be simple - while this isn't particularly realistic, similar effects are commonly seen in real apps where actions call an API endpoint (and even though one hopes that doesn't normally take as long as 3 seconds, the same problems can always be observed with quicker requests - I've just slowed it down to be easier to trigger the bug).

To see the difference, try the following with each of the 2 bottom buttons:

  • click the button
  • click the button on top (to increment the count again) BEFORE the 3-second timeout is up

You should see a clear difference in behaviour:

  • with "approach 1" (button on the right, which I'm calling "good" here), the count increments a second time after the timeout is finished.
  • with "approach 2" (button on the left, which I've called "bugged"), there is no further increment from the value produced by the intermediate click on the top button, no matter how long you wait

(You can see this more dramatically if you click the bottom button multiple times quickly, then the top one once. And for an even more counterintuitive effect, try pressing the "bugged" bottom button one or more times, then clicking the top button more than once, all within the 3 second time interval.)

Why does this happen? Well, the "buggy" behaviour happens because the function inside the setTimeout is a closure over the outer variable count which is in the scope of the full component function. That means that when it's called with count + 1 as the argument, it will update the count to 1 more than whatever it was at the point the function was defined. Say you do the above sequence from first loading the component where count is 0, then the more detailed sequence of what happens is:

  • the bottom button click schedules a callback to happen in 3 seconds' time. Since count at that point is equal to 0, its argument, count + 1 is equal to 1.
  • the top button click rerenders the component, with count now equal to 1.
  • the callback set up at the first step later triggers, and sets the count to 1. This doesn't cause any noticeable chance, because the count was already 1. (If you tried clicking the top button multiple times, so it now shows 2 or more, this will actually decrement the counter, because as I'm explaining, it will always get set to 1.)

If you know a little bit about JS closures, you might wonder why the count that is accessed in the closure is still 0. Wasn't it previously updated to 1? No, it wasn't, and that's the bit that might be counterintuitive. Notice how count is declared with const? That's right - it never actually changes. The reason the UI updates is because setCount causes React to rerender your component, which means the whole outer function corresponding to the component is called again. This sets up a whole new environment, with a new count variable. React's internals ensure that the useState call now gives back 1 for the current count, which is therefore the value in that new "instance" of the component - but that's irrelevant from the point of view of the function that was put in the event queue to fire after 3 seconds. As far as it's concerned, the count variable - no longer in scope but "remembered" inside that callback as all closed-over variables are - has never changed from 0. The count that's equal to 1 is in a different scope entirely, and forever inaccessible to that first callback.

How does the function argument form - "approach 1" - get round this? Very easily. It doesn't hold any closure at all - the variable inside that function, which I've called oldCount here for the sake of both accuracy and to disambiguate from the outer count - has nothing to do with the count outside. It's the argument to a function that React itself will call internally. When React does call the function, it always supplies the "most up-to-date" state value it has. So you don't have to worry about "stale closures" or anything like that - you're saying "whatever the most recent value was, update the count to be one more than that", and React will take care of the rest.

I've called approach 2 "bugged" here because I think it's reasonable to expect an increment to happen after the timeout, if you've clicked a button that you've set up to do an increment. But this isn't always what you want. If you genuinely wanted the update to be based on the value at the point the button was first clicked, then of course you will prefer Approach 2, and Approach 1 will seem bugged. And in a sense that's more often the case. I highly recommend reading this post by Dan Abramov - one of the core React developers - that explains a crucial difference between class components and functions, that's based on many of the same arguments about closures that came into play here, where normally you do want the event handlers to reference values as they were at the time of render, not when they actually fire after an API request or timeout.

But that post doesn't have anything to do with the "approach 1" form of state-updating functions, which isn't even mentioned in the article. That's because it's irrelevant to the examples given - there'd be no (sensible) way to rewrite those examples to use it. But when you do want to update a state value based on its previous value - as could happen with negating a boolean value, as in the OP example, or incrementing a counter as in mine, I would argue that it's more natural that you always want that "previous value" to be up to date. There are 2 buttons which both should increment a value, albeit in different ways - I think it's reasonable to call it bugged if clicking both of them, depending on the timing, may only increment once in total.

But that's of course up to each individual component or application to decide. What I hope I've done here is explain what the difference is, and give you a basis to choose which might be best. But I do believe that 90+% of the time, if you have the option of using the function argument ("approach 1"), it will be better, unless you know it isn't.

jsejcksn
  • 27,667
  • 4
  • 38
  • 62
Robin Zigmond
  • 17,805
  • 2
  • 23
  • 34
  • What you haven't shown is updating the value without a delay, such as `const incrementWithoutTimeout = () => setCount(count + 1);`. This works fine, so any reason not to do it this way? Furthermore, any reason not to include this as a possibility since it seems very relevant to the question? – h0r53 May 04 '23 at 21:33
  • 1
    It works fine in simple cases. It may not in more complex ones where other renders happen in between. The timeout was just the simplest way to illustrate such cases. As I said, it depends on your actual intention - if you always want to update the state based on its most recent value, the callback form is guaranteed to work, while if you want to update it based on the value at the time of the particular render when the user took an action, the non-callback form is best. Both situations come up in practice. – Robin Zigmond May 04 '23 at 21:42
5

the first approach setIsChanged((prevState) => !prevState)

to make sure that you always have the last state before changing it.

Amr yasser
  • 408
  • 5
  • 10
  • As per some comments on the question and your answer, I know that this is async operation, but I also assume that there is no state is missed otherwise it would be a big mess. If that is the case, I'm unable to understand how exactly 1st approach is better. – Swanand Jan 03 '22 at 15:40
  • Sorry for being late, well you can use both approaches and they will work fine. but if we used more than one state and their values changes based on some conditions so we end up without knowing the last state that it ends up with. to be specific with the last state we have for a specific state then react provides for us the prevState inside the setState so we can say hi please take the last state that this state reached for and update it to a new one. you might say what is the big difference but it help sometimes to take the last state or to know what is it. – Amr yasser Jan 04 '22 at 18:15
0

The simple answer is this:

setIsChanged((prevState) => !prevState) In this case the setter setIsChanged provided by the useState hook is passing it's own internal reference value, so it's always update, despite the whole async issue with useState.

In short, you're using the internal state value with prevState.

setIsChanged(!isChanged) In this case, you're using the value provided by the useState hook to your COMPONENT, but not an internal ref. In this case you're working with data locally in your component that could be stale due to how components work asynchronously.

For most cases, the second approach works just fine and is by far the most common. You'll only use the second one if you're updating the state in several places at once or feel it could get out of sync.

You can read more about that here

Thales Kenne
  • 2,705
  • 1
  • 12
  • 26
0

State updates are asynchronous which means they are not updated immediately. They are scheduled. If your state depends on previous state then use 1st approach.

if your updatedState does not depend on previous state used 2nd approach. Example:

  1. Button Click to increase counter or toggle between states. approach 1
  2. Resetting input field after submit button is clicked. approach 2
0

useState is async so sometimes this way does not work

 setEditing(!prevState) 

first the all we need the previous value, i think this way is best

 setEditing((prevState) => !prevState)  
rnewed_user
  • 1,386
  • 7
  • 13
-1

you can try custom hooks

useToggle.js

import { useReducer } from 'react';

function toggler(currentValue, newValue) {
  return typeof newValue === 'boolean' ? newValue : !currentValue;
}

function useToggle(initialValue = false) {
  return useReducer(toggler, initialValue);
}

export default useToggle;

use like

import useToggle from './useToggle';

const App = () => {
  const [isShown, toggle] = useToggle();
  return <button>{isShown ? 'show' : 'hide'}</button>;
};
export default App;
Rajesh kumar R
  • 214
  • 1
  • 8