reactjsnext.jsreact-hooksmouseevent

How can I prevent useEffect from removing a similar effect?


I have a custom hook that detects whether a user clicks inside a given element or somewhere else. It works fine when used alone on a single page, but when I use two instances of the hook, the useEffect cleanup function removes the other event listener. How can I fix this?

"use client";

import { useCallback } from "react";
import { useEffect, useState } from "react";

export default function useOutSideAlert(inside: React.RefObject<HTMLDivElement | null>, handlerToRun: (inside: boolean) => void) {
  const [isInside, setInside] = useState(false);

  const handler = useCallback(
    (e: MouseEvent) => {
      const itemClicked = e.target as HTMLElement;
      if (inside.current && inside.current.contains(itemClicked)) {
        handlerToRun(true);
        setInside(true);
      } else {
        handlerToRun(false);
        setInside(false);
      }
    },
    [handlerToRun, inside]
  );
  useEffect(() => {
    document.addEventListener("click", handler);
    return () => document.removeEventListener("click", handler);
  }, [handler, inside]);
  return isInside;
}

How I am use this Hook

import { FontStyles } from "@/components/templates/ReadBookComponent";
import React, { useRef, useState } from "react";
import useOutSideAlert from "./useOutsideAlart";

export default function useHiddenNav(navBar: React.RefObject<HTMLDivElement | null>) {
  const [displayNav, setDisplayNav] = useState(false);
  const [fontSize, setFont] = useState<number>(22);
  const [fontStyle, setfonstStyle] = useState<FontStyles>(FontStyles.serif);
  const timer = useRef<NodeJS.Timeout>(null);
  const isInside = useOutSideAlert(navBar, showBottomNav);
  void isInside;

  function showBottomNav(inside: boolean) {
    if (timer.current) {
      clearTimeout(timer.current);
    }
    if (inside) {
      setDisplayNav(true);
    } else {
      setDisplayNav(!displayNav);
      timer.current = setTimeout(() => {
        setDisplayNav(false);
      }, 3000);
    }
  }

  return [{ fontSize, setFont, fontStyle, setfonstStyle }, displayNav] as const;
}

export default function ReadBookComponent({ searchParams }: ReadBookComponentType) {
  const navBar = useRef<HTMLDivElement>(null);
  const topNavBar = useRef<HTMLDivElement>(null);
  const [font, displayTopNav] = useHiddenNav(topNavBar);
  void font;
  const [componentFont, displayNav] = useHiddenNav(navBar);
return(
<>
 <div ref={topNavBar} style={{ display: displayTopNav ? "" : "none" }} className="w-screen">
   <div className="bg-themeSuperDark/70 h-20  flex items-center m-0  justify-between px-6 fixed top-0 w-screen z-40"></div>
 </div>
 <div ref={navBar} style={{ display: displayNav ? "" : "none" }} className="w-screen ">
  <BookHiddenNav componentFont={componentFont} setDisplayCommets={setDisplayCommets} />
  </div>
</>
)

Solution

  • Juding by the code, it seems it could be function handler did not update when the handler.current (DOM object) is updated but ref object handler itself isn't. Which causes the updated DOM object did not set up the listener properly since the useEffect didn't trigger.

    You could try using handler.current as one of its dependency for the useCallback hook as so:

    export default function useOutSideAlert(inside: React.RefObject<HTMLDivElement | null>, handlerToRun: (inside: boolean) => void) {
      const [isInside, setInside] = useState(false);
    
      const handler = useCallback(
        (e: MouseEvent) => {
          const itemClicked = e.target as HTMLElement;
          if (inside.current && inside.current.contains(itemClicked)) {
            handlerToRun(true);
            setInside(true);
          } else {
            handlerToRun(false);
            setInside(false);
          }
        },
        [handlerToRun, inside.current] // <- use inside.current instead of the inside ref object itself
      );
      useEffect(() => {
        document.addEventListener("click", handler);
        return () => document.removeEventListener("click", handler);
      }, [handler, inside]);
      return isInside;
    }