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.