Do you want to request a feature or report a bug?
Bug/Question
What is the current behavior?
When I retrieve a value from a useState hook inside a setTimeout function, the value is the one when the function was called and not when the code inside gets executed.
You can try here, just increase the counter then start the timeout and increase the counter again before the timeout expires.
https://codesandbox.io/s/2190jjw6op
What is the expected behavior?
Retrieving the updated state.
If instead it's working as intended how I can retrieve the updated status?
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
16.7.0-alpha.0
This is subtle but expected behavior. When setTimeout
is scheduled it's using the value of count
at the time it was scheduled. It's relying on a closure to access count
asynchronously. When the component re-renders a new closure is created but that doesn't change the value that was initially closed over.
If instead it's working as intended how I can retrieve the updated status?
In this case you need to use a "container" that you can write the updated state value to, and read it later on in the timeout. This is one use case for useRef
. You can sync the state value with the ref's current
property and read current
in the timeout instead.
// Use a ref to access the current count value in
// an async callback.
const countRef = useRef(count);
countRef.current = count;
const getCountTimeout = () => {
setTimeout(() => {
setTimeoutCount(countRef.current);
}, 2000);
};
I'd note that in many cases this is the _desired_ behavior. For example if you subscribe to an ID, and later want to unsubscribe, it would be a bug if ID could change over time. Closing over the props/state solves those categories of issues.
And if you need modify state itself in timeout/interval you should not use state from closure, instead get it like that:
const [count, setCount] = React.useState(0);
React.useEffect( () => {
const i_id = setInterval(() => {
setCount(currCount => currCount+1)
},3000);
return () => {
clearInterval(i_id);
}
},[]);
As @strobox mentioned, providing a callback when using the function returned from useState is really efficient for this. I'm using it with a setTimeout to hide notifications after an animation:
const [notifications, setNotifications] = useState<Notification[]>([]);
useEffect(() => {
const listener = setMessageHook(MessageType.Error, (resp: ErrorBody) => {
const uid = getUid();
setNotifications((notifs) =>
addNotification(notifs, { id: uid, value: resp.error }),
);
// ----- Interesting part
setTimeout(
() => setNotifications((notifs) => removeNotification(notifs, uid)),
animationDuration * 1000,
);
// -----------------------
});
return function cleanup() {
chrome.runtime.onMessage.removeListener(listener);
};
}, []);
I hope it will help future people with this issue. (Thanks @strobox )
Most helpful comment
This is subtle but expected behavior. When
setTimeout
is scheduled it's using the value ofcount
at the time it was scheduled. It's relying on a closure to accesscount
asynchronously. When the component re-renders a new closure is created but that doesn't change the value that was initially closed over.In this case you need to use a "container" that you can write the updated state value to, and read it later on in the timeout. This is one use case for
useRef
. You can sync the state value with the ref'scurrent
property and readcurrent
in the timeout instead.https://codesandbox.io/s/6zo5y2p8qk