setTimeout is not reset in React JS

Viewed 56

I created a hook to be able to set a timeout. Every callback that will be added in that hook will be delayed with a certain amount of time:

import "./styles.css";
import React, { useRef, useCallback, useState, useEffect } from "react";
const useTimer = () => {
  const timer = useRef();
  const fn = useCallback((callback, timeout = 0) => {
    timer.current = setTimeout(() => {
      callback();
    }, timeout);
  }, []);

  const clearTimeoutHandler = () => {
    console.log("clear");
    return clearTimeout(timer);
  };

  useEffect(() => {
    return clearTimeoutHandler();
  }, []);

  return { fn, clearTimeoutHandler };
};

export default function App() {
  const [state, setState] = useState(false);
  const timer = useTimer();
  const onClickHandler = () => {
    setState(true);
    timer.fn(() => {
      console.log(5000);
      setState(false);
    }, 5000);
  };
  return (
    <div className="App">
      {state && <h1>Hello</h1>}
      <button onClick={onClickHandler}>Open</button>
    </div>
  );
}

In my case Hello text will appear after user will click on the button and will disappear after 5000 sec.
Issue: After clicking 1 time on the button and after 3 sec again clicking on it i expect to see Hello text 5 sec right after the last click, but in my case if i click twice the timer will take into account only the first click and if i will click after 3 sec one more time the text will disappear after 2 sec not 5.
Question: how to fix the hook and to reset the timer if the user click many times or the hook is called many times?
demo: https://codesandbox.io/s/recursing-ride-mlqnri?file=/src/App.js:0-880

3 Answers

All you need is to add clearTimeout() to your useTimer() function.

Here's your working code:

import "./styles.css";
import React, { useRef, useCallback, useState, useEffect } from "react";
const useTimer = () => {
  const timer = useRef();
  const fn = useCallback((callback, timeout = 0) => {
    // Add this line here
    clearTimeout(timer.current);
    timer.current = setTimeout(() => {
      callback();
    }, timeout);
  }, []);

  const clearTimeoutHandler = () => {
    console.log("clear");
    return clearTimeout(timer);
  };

  useEffect(() => {
    return clearTimeoutHandler();
  }, []);

  return { fn, clearTimeoutHandler };
};

export default function App() {
  const [state, setState] = useState(false);
  const timer = useTimer();
  const onClickHandler = () => {
    setState(true);
    timer.fn(() => {
      console.log(5000);
      setState(false);
    }, 5000);
  };
  return (
    <div className="App">
      {state && <h1>Hello</h1>}
      <button onClick={onClickHandler}>Open</button>
    </div>
  );
}

And a sandbox demo: https://codesandbox.io/s/angry-dream-xmzh9v?file=/src/App.js

You are passing the ref directly to clearTimeout

 const clearTimeoutHandler = () => {
    console.log("clear");
    return clearTimeout(timer); // should be timer.current
  };
Related