patternjavascriptreactCritical
Accessing up-to-date state from within a callback
Viewed 0 times
callbackfromdatestateaccessingwithin
Problem
We use a third party library (over which there is limited control) that takes a callback as argument to a function. What is the correct way to provide that callback with the latest state? In class components, this would be done through the use of
With the following code, every time the callback accesses it, it's back at its default value. The console will keep printing
CodePen
I've had no problem setting state within a callback, only in accessing the latest state.
If I was to take a guess, I'd think that any change of state creates a new instance of the Card function. And that the callback is referring to the old one. Based on the documentation at https://reactjs.org/docs/hooks-reference.html#functional-updates, I had an idea to take the approach of calling setState in the callback, and passing a function to setState, to see if I could access the current stat
this. In React hooks, due to the way state is encapsulated in the functions of React.useState(), if a callback gets the state through React.useState(), it will be stale (the value when the callback was setup). But if it sets the state, it will have access to the latest state through the passed argument. This means we can potentially get the latest state in such a callback with React hooks by setting the state to be the same as it was. This works, but is counter-intuitive.With the following code, every time the callback accesses it, it's back at its default value. The console will keep printing
Count is: 0 no matter how many times I click.function Card(title) {
const [count, setCount] = React.useState(0)
const [callbackSetup, setCallbackSetup] = React.useState(false)
function setupConsoleCallback(callback) {
console.log("Setting up callback")
setInterval(callback, 3000)
}
function clickHandler() {
setCount(count+1);
if (!callbackSetup) {
setupConsoleCallback(() => {console.log(`Count is: ${count}`)})
setCallbackSetup(true)
}
}
return (
Active count {count}
Increment
);
}
const el = document.querySelector("#root");
ReactDOM.render(, el);CodePen
I've had no problem setting state within a callback, only in accessing the latest state.
If I was to take a guess, I'd think that any change of state creates a new instance of the Card function. And that the callback is referring to the old one. Based on the documentation at https://reactjs.org/docs/hooks-reference.html#functional-updates, I had an idea to take the approach of calling setState in the callback, and passing a function to setState, to see if I could access the current stat
Solution
For your scenario (where you cannot keep creating new callbacks and passing them to your 3rd party library), you can use
Your callback can refer to the mutable object to "read" the current state. It will capture the mutable object in its closure, and every render the mutable object will be updated with the current state value.
useRef to keep a mutable object with the current state. Like so:function Card(title) {
const [count, setCount] = React.useState(0)
const [callbackSetup, setCallbackSetup] = React.useState(false)
const stateRef = useRef();
// make stateRef always have the current count
// your "fixed" callbacks can refer to this object whenever
// they need the current value. Note: the callbacks will not
// be reactive - they will not re-run the instant state changes,
// but they *will* see the current value whenever they do run
stateRef.current = count;
function setupConsoleCallback(callback) {
console.log("Setting up callback")
setInterval(callback, 3000)
}
function clickHandler() {
setCount(count+1);
if (!callbackSetup) {
setupConsoleCallback(() => {console.log(`Count is: ${stateRef.current}`)})
setCallbackSetup(true)
}
}
return (
Active count {count}
Increment
);
}Your callback can refer to the mutable object to "read" the current state. It will capture the mutable object in its closure, and every render the mutable object will be updated with the current state value.
Code Snippets
function Card(title) {
const [count, setCount] = React.useState(0)
const [callbackSetup, setCallbackSetup] = React.useState(false)
const stateRef = useRef();
// make stateRef always have the current count
// your "fixed" callbacks can refer to this object whenever
// they need the current value. Note: the callbacks will not
// be reactive - they will not re-run the instant state changes,
// but they *will* see the current value whenever they do run
stateRef.current = count;
function setupConsoleCallback(callback) {
console.log("Setting up callback")
setInterval(callback, 3000)
}
function clickHandler() {
setCount(count+1);
if (!callbackSetup) {
setupConsoleCallback(() => {console.log(`Count is: ${stateRef.current}`)})
setCallbackSetup(true)
}
}
return (<div>
Active count {count} <br/>
<button onClick={clickHandler}>Increment</button>
</div>);
}Context
Stack Overflow Q#57847594, score: 308
Revisions (0)
No revisions yet.