Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React UseState - using previous state vs not using previous state [duplicate]

I was wondering what the difference is between both examples below. In one example I use the previous state and in the other example I directly use the current value. They both give me the same results. In which cases should I use one way over the other? Thanks in advance.

import React,{useState} from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  const [count, setCount] = useState(0);
  const [count2, setCount2] = useState(0);
  return (
    <div className="App">
      Count: {count}
      <button onClick={() => setCount(0)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      <br/>
      <br/>
      Count: {count2}
      <button onClick={() => setCount2(0)}>Reset</button>
      <button onClick={() => setCount2(count2 - 1)}>-</button>
      <button onClick={() => setCount2(count2 + 1)}>+</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
like image 549
galeontiger Avatar asked Jun 02 '26 00:06

galeontiger


1 Answers

Because those calls to the state setter are in click handlers, your component is guaranteed to be re-rendered before another click is processed. For that reason, in most cases you don't have to use the callback version of the setter, you can directly use your existing state. (Even in concurrent mode.) (Note that if you handle the same click in more than one place [an element and a descendant of it, for instance], and you want both of those handlers to update the value, that's a different matter — see skyboyer's answer for an example of that.)

This is not true for all events (mousemove, for instance, does not have this guarantee), but it's true for click.

I got this information from Dan Abramov on twitter in this thread. At the time, events like click that had this guarantee were called "interactive" events. The name has since changed to "discrete" events. You can find a list in this source file in the React code.

Of course, not all state changes come directly from events. Suppose you have a click handler in your code that does a couple of ajax calls in series and, it happens, updates your value in response to completing each of them. The direct update version will be incorrect even if you've tried to be really thorough with useCallback; the callback version will be correct:

const {useState, useCallback} = React;

function ajaxGet() {
    return new Promise(resolve => setTimeout(resolve, 10));
}

function Example() {
    const [directValue, setDirectValue] = useState(0);
    const [callbackValue, setCallbackValue] = useState(0);

    const doThis = useCallback(() => {
        setDirectValue(directValue + 1);
        setCallbackValue(callbackValue => callbackValue + 1);
    }, [directValue, callbackValue]);
    
    const doThat = useCallback(() => {
        setDirectValue(directValue + 1);
        setCallbackValue(callbackValue => callbackValue + 1);
    }, [directValue, callbackValue]);

    const handleFirstFulfilled = useCallback(() => {
        // ...
        doThis();
        // ...
        return ajaxGet("something else");
    }, [doThis]);
    
    const handleSecondFulfilled = useCallback(() => {
        // ...
        doThat();
        // ...
    }, [doThat]);
    
    const handleClick = useCallback(() => {
        ajaxGet("something")
        .then(handleFirstFulfilled)
        .then(handleSecondFulfilled)
        .catch(error => {
            // ...handle/report error...
        });
    }, [handleFirstFulfilled, handleSecondFulfilled]);

    const cls = directValue !== callbackValue ? "diff" : "";

    return (
        <div className={cls}>
          <input type="button" onClick={handleClick} value="Click Me" />
          <div>
          Direct: {directValue}
          </div>
          <div>
          Callback: {callbackValue}
          </div>
        </div>
    );
}

ReactDOM.render(<Example />, document.getElementById("root"));
.diff {
    color: #d00;
}
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>

(Disclaimer: That code may be utter rubbish. The point is to see the effect despite having tried to memoize everything. :-) )

For that reason, any time I'm setting a new value that's based on the previous value, I use the callback version unless it's a dedicated click handler or similar, in which case I may go direct.


Getting back to events, concurrent mode makes non-"discrete" events easier to stack up. In the current version of React on cdnjs.com (v16.10.2), I cannot get the following to have different numbers for directValue, callbackValue, and manualValue:

const {useState} = React;

// Obviously this is a hack that only works when `Example` is used only once on a page
let manualValue = 0;
const manualDisplay = document.getElementById("manualDisplay");
function Example() {
    const [directValue, setDirectValue] = useState(0);
    const [callbackValue, setCallbackValue] = useState(0);
    
    const handleMouseMove = () => {
        setDirectValue(directValue + 1);
        setCallbackValue(callbackValue => callbackValue + 1);
        manualDisplay.textContent = ++manualValue;
    };

    const different = directValue !== callbackValue || directValue !== manualValue;
    document.body.className = different ? "diff" : "";

    return (
        <div onMouseMove={handleMouseMove}>
          Move the mouse rapidly over this element.
          <div>
          Direct: {directValue}
          </div>
          <div>
          Callback: {callbackValue}
          </div>
        </div>
    );
}

const ex = <Example />;
if (ReactDOM.createRoot) {
    document.body.insertAdjacentHTML("beforeend", "<div>Concurrent</div>");
    ReactDOM.createRoot(document.getElementById("root")).render(ex);
} else {
    ReactDOM.render(ex, document.getElementById("root"));
    document.body.insertAdjacentHTML("beforeend", "<div>Legacy</div>");
}
.diff {
    color: #d00;
}
<div id="root"></div>
<div>
Manual: <span id="manualDisplay">0</span>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>

Maybe that's just me not testing on enough platforms, but I can't get them to diverge in React's "legacy mode." But, using that same code with the experimental release with concurrent mode, it's fairly easy to get the directValue to lag behind the callbackValue and manualValue by waggling the mouse quickly over it, indicating that the event handler is running more than once between renders.

like image 149
T.J. Crowder Avatar answered Jun 05 '26 02:06

T.J. Crowder



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!