Search code examples
javascriptreactjsecmascript-6

clearTimeout a timer in a function with another function (React.js)


The code is for a Navbar. I want this when the mouse got out of it this timer started, but while this time was counting if I moved my mouse over the Navbar, the timer counting being stopped and changed displayed to none dont being applied. The problem is when I move the mouse out of the Navbar and then over it again the timer does not stop and by the end of the counting dropdown display is gone.

here is my code:

const [navOver, setNavOver] = useState(false);
const [currentItem, setCurrentItem] = useState("");

let timer;

const handleMouseOver = (item) => {
  setNavOver(true);
  clearTimeout(timer);
  for (var i = 0; i < dropdown.length; i += 1) {
    if (dropdown[i].style.display === "none") {
      dropdown[i].style.display = "block";
    }
  }
  if (!currentItem) {
    setCurrentItem(item);
  }
};

const handleMouseOut = (event) => {
  if (!event.relatedTarget || !event.relatedTarget.closest("nav")) {
    setNavOver(false);
    timer = setTimeout(() => {
      for (var i = 0; i < dropdown.length; i += 1) {
        dropdown[i].style.display = "none";
      }
    }, 700);
  }
};

and jsx code:

<nav onMouseOut={handleMouseOut}>
  <ul className="lg:flex hidden gap-4 ps-4">
    {navLinks.map((item, index) => {
      return (
        <>
          <li key={index} className="z-50">
            <Link
              to={item.href}
              onMouseOver={() => {
                handleMouseOver(item);
                setCurrentItem(item);
              }}
            >
              {item.label}
            </Link>
          </li>
          <div className="dropdownContainer">
            // there was dropdown component
          </div>
        </>
      );
    })}
  </ul>
</nav>

I wanna stop the timer in handleMouseOut using the handleMouseOver function.


Solution

  • Since let timer; is declared in the body of the component, it's being re-defined every render. So by the time handleMouseOver is trying to run clearTimeout(timer); timer is actually undefined. This is a case where a ref could be used to hold the value/referance of timer between renders.

    so doing this should get what you want.

    const timerRef=useRef(null);
    
    const handleMouseOver = (item) => {
      setNavOver(true);
      if(timerRef.current) clearTimeout(timerRef.current);
      
      for (var i = 0; i < dropdown.length; i += 1) {
        if (dropdown[i].style.display === "none") {
          dropdown[i].style.display = "block";
        }
      }
      if (!currentItem) {
        setCurrentItem(item);
      }
    };
    
    const handleMouseOut = (event) => {
      if (!event.relatedTarget || !event.relatedTarget.closest("nav")) {
        setNavOver(false);
        timerRef.current = setTimeout(() => {
          for (var i = 0; i < dropdown.length; i += 1) {
            dropdown[i].style.display = "none";
          }
        }, 700);
      }
    };
    

    You're also potentially creating a memory leak, so you should also clean up the timer if the page un-mounts, by cleaning it up in the return of a useEffect like so.

    useEffect(()=>{
       return()=> {
         if(timerRef.current) clearTimeout(timerRef.current);
         }
    },[])