Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

State from useState hook inside a setTimeout is not updated #14010

Closed
federico-moretti opened this issue Oct 28, 2018 · 8 comments
Closed

State from useState hook inside a setTimeout is not updated #14010

federico-moretti opened this issue Oct 28, 2018 · 8 comments

Comments

@federico-moretti
Copy link

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

@aweary
Copy link
Contributor

aweary commented Oct 29, 2018

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

@aweary aweary closed this as completed Oct 29, 2018
@gaearon
Copy link
Collaborator

gaearon commented Oct 29, 2018

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.

@strobox
Copy link

strobox commented Mar 2, 2019

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);
    }
  },[]);

@terry90
Copy link

terry90 commented Mar 21, 2019

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 )

@NguyenfromVN
Copy link

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

another way does not use useRef is that you define a variable outside the react function component, and at the beginning of the function component, you need to update that variable to match the lastest state value, that is, no need to use useRef if you find it easier by this way, happy coding!

@cglacet
Copy link

cglacet commented Jun 4, 2021

Not sure if that's a good strategy, but in a similar case we used the following idea:

import { useImperativeHandle, useRef, useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);
  const [timeoutCount, setTimeoutCount] = useState(null);

  const ref = useRef(null);
  useImperativeHandle(ref, () => ({
    onTimeout: () => setTimeoutCount(count),
  }));

  const getCountTimeout = () => {
    setTimeout(() => ref?.current?.onTimeout(), 2000);
  };

  return (
    <div className="App">
      Count: <button onClick={() => setCount(count + 1)}>{count}</button>
      <br />
      <br />
      <button onClick={getCountTimeout}>Get count with timeout</button>
      <br />
      <p>Count from timeout: {timeoutCount}</p>
    </div>
  );
}

In our case that's more practical than using a ref to the state as suggested before, mainly because our state is quite large.

@omidgharib
Copy link

same issue here

@Maverick-1707
Copy link

Maverick-1707 commented Dec 6, 2023

Would this be bad practice as a workaround for this issue and avoid creating a ref?

const [counter, setCounter] = useState(0)
const [timerCounter, setTimerCounter] = useState(0)

useEffect(() => {
  const timer = setTimeout(() => {
    setCounter(currentValue => {
      setTimerCounter(currentValue)

      return currentValue
    })
  }, 2000)
  setCounter(5)

  return () => clearTimeout(timer)
}, [])

This works and when the timer code is executed, both counters have the same value (5). Basically, we make sure to access the current value of counter and not the value at the time the timer is scheduled by accessing it inside setCounter and setting the state of the timerCounter inside it as well, and then just making sure to return the currentValue in setCounter so "counter" is not affected or even causing a new render.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants