React: State from useState hook inside a setTimeout is not updated

Created on 28 Oct 2018  路  4Comments  路  Source: facebook/react

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

Most helpful comment

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);
};

https://codesandbox.io/s/6zo5y2p8qk

All 4 comments

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);
};

https://codesandbox.io/s/6zo5y2p8qk

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 )

Was this page helpful?
0 / 5 - 0 ratings