typescriptreact-nativereact-hookssettimeoutcleartimeout

React-Native setTimeout function triggered after clearTimeout is called


In my react-native app, I am building a component that has a 'heads up display' with some controls, that only appears after touching the view. Then, one second after the last interaction of the user, I want those buttons to disappear again for a cleaner look.

I am using setTimeout for this. Whenever the user interacts with my component, I trigger a function. In this function, I set a timeout for 1 second, and then clear the navigation buttons when it triggers. I also check if another timer was already set (I store them in state), and cancel it because I want to make sure that only the latest timeout function gets triggered.

This is the code:

const [timer, setTimer] = useState<number>(null)

const handleControlsInteraction = useCallback(() => {
    // If the user interacted with the controls, we need to make them disappear 1 second after the latest interaction

    // If a timer was already set by a previous interaction, we want to cancel it, 
    // because because the controls should disappear 1 second after the latest interaction
    if (timer != null) {
        console.log('Cancelling timeout: ', timer)
        clearTimeout(timer)
    }

    // now we set a new timer for 1 second
    let timeout = setTimeout(() => {
        // When the function of the timer triggers, we need to hide the controls, clear the timeout and remove the timout from our local state
        console.log('Hiding buttons for timeout: ', timeout)
        setDisplayButtons(false)
        clearTimeout(timer)
        setTimer(null)
    }, 1000);
    console.log('Setting new timeout: ', timeout)

    setTimer(timeout)
}, [timer])

When I run this, And I tap my control twice (half a second apart), I notice that my first timer gets cancelled as expected, but it's function still gets triggered after that. This causes my controls to get hidden when the user is still interacting with them, rather than 1 second after the latest interaction.

What I'm trying to achieve seems like a pretty basic thing to do. However, I've been puzzling with this for a while now.

How can I resolve this? Any guidance would be highly appreciated.


Solution

  • When your component changes the state and rerender, a new timeout instance recreates and results in unexpected behavior.

    The solution is to keep track of the original timeout instance for each component rerender cycle.

    React provide useRef hook to persist value for each component render cycle.

    let timerRef = React.useRef(null);
    
    useEffect(() => {
      // Clear the interval when the component unmounts
      return () => clearTimeout(timerRef.current);
    }, []);
    
    const handleControlsInteraction = () => {
    
      if (timerRef.current != null) {
        clearTimout(timerRef.current)
      }
    
      // now we set a new timer for 1 second
      timerRef.current = setTimeout(() => {
        setDisplayButtons(false);
      }, 1000);
    };