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>
</>
)
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;
}