3

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);
  • @Chris Please don't use your dupe-hammer to point to your own answers. There are already great duplicates like [When to use React setState callback](https://stackoverflow.com/q/42038590/1218980). – Emile Bergeron Nov 27 '19 at 16:19
  • @EmileBergeron Well, first of all the question *is* a duplicate so closing it is justified. Second, the question I pointed to was answered the other day and was hence the easiest relevant question to find. Third, I find my answer more useful because it has a working demo of how the two approaches differ, whereas the one you suggested didn't. I think it was a perfectly valid duplicate and closing it was justified. If you disagree you could always raise a flag for mod review. – Chris Nov 27 '19 at 16:49
  • @ galeontiger - FYI, I've updated my answer to add more details. – T.J. Crowder Nov 27 '19 at 18:18

2 Answers2

3

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.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 1
    While you are entirely correct, I always advocate the use of the updater function whenever the new state has an old state dependency. – Chris Nov 27 '19 at 15:33
  • @Chris - Yeah, I lean that way too... – T.J. Crowder Nov 27 '19 at 15:52
  • Does mousemove not have this guarantee due to its usual high rate of trigger (not sure how to express this better) or is it because it's a less "explicit" user interaction, thus being less-significant, so-to-speak, event? I didn't know this, and now you got me curious – Chris Nov 27 '19 at 17:13
  • 1
    @Chris - I'm afraid I don't know, but I suspect it's along those lines. – T.J. Crowder Nov 27 '19 at 18:19
2

For your example there is no difference. But there are cases when that matters.

const [val, setVal] = useState(0);

return (<div onClick={() => setVal(val + 1)}>
 <span onClick={() => setVal(val + 1)}>{val}</span>
</div>);

will increment value only by 1 per click(0 -> 1 -> 2 -> 3). Live Example:

const {useState} = React;

function Example() {
  const [val, setVal] = useState(0);

  return (<div onClick={() => setVal(val + 1)}>
   <span onClick={() => setVal(val + 1)}>{val}</span>
  </div>);
}

ReactDOM.render(<Example/>, document.getElementById("root"));
<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>
const [val, setVal] = useState(0);

return (<div onClick={() => setVal(oldVal => oldVal + 1)}>
 <span onClick={() => setVal(oldVal => oldVal + 1)}>{val}</span>
</div>);

will increment value by 2 per click(0 -> 2 -> 4 -> 6). Live Example:

const {useState} = React;

function Example() {
  const [val, setVal] = useState(0);

  return (<div onClick={() => setVal(oldVal => oldVal + 1)}>
   <span onClick={() => setVal(oldVal => oldVal + 1)}>{val}</span>
  </div>);
}

ReactDOM.render(<Example/>, document.getElementById("root"));
<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>
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
skyboyer
  • 22,209
  • 7
  • 57
  • 64
  • That's true if you're handling the **same** click at more than one level, as in your code. Good point. It's unusual, but it's still a good point. – T.J. Crowder Nov 27 '19 at 15:58
  • yeah, it's rather synthetic case. I see keeping all state updates as callbacks(once they rely on previous value) is good move anyway. Easier to update(like moving into `useEffect` mentioned above). – skyboyer Nov 27 '19 at 16:04