Reputation: 23
export const useOutsideElementClick = (
element: HTMLElement | null,
handler: (target: EventTarget | null) => void,
relativeOutsideElementSelector?: string,
) => {
useLayoutEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (element && !element.contains(event.target as Node)) {
handler(event.target);
}
};
const modal = relativeOutsideElementSelector
? (document.querySelector(relativeOutsideElementSelector) as HTMLElement)
: (document as unknown as HTMLElement);
modal?.addEventListener('click', handleClickOutside);
return () => {
modal?.removeEventListener('click', handleClickOutside);
};
}, [element]);
};
when I include this hook in component, when there is a div with ref which disappears and show with condition when some of value with setState change useEffect doesn't run again. I check "element" value in useOutsideElementClick block with console.log. It changes from null to some HTMLDivElement, but useEffect doesn't react
Upvotes: 1
Views: 108
Reputation: 191976
Refactor the hook, so it would return a setElement
function instead of expecting an element that you get from a ref
. Ref changes don't cause re-render, and that's why the element change is ignored.
The setElement
is return from useState
. Calling setElement
would cause the a re-render, and would change the element
that would trigger the useEffect
:
export const useOutsideElementClick = (
handler: (target: EventTarget | null) => void,
relativeOutsideElementSelector?: string,
) => {
const [element, setElement] = useState<HTMLElement | null>(null);
useLayoutEffect(() => {
if(!element) return; // skip if no element
const handleClickOutside = (event: MouseEvent) => {
if (element.contains(event.target as Node)) {
handler(event.target);
}
};
const modal = relativeOutsideElementSelector
? (document.querySelector(relativeOutsideElementSelector) as HTMLElement)
: (document as unknown as HTMLElement);
modal?.addEventListener('click', handleClickOutside);
return () => {
modal?.removeEventListener('click', handleClickOutside);
};
}, [element, handler, relativeOutsideElementSelector]);
const return setElement;
};
Usage - you use the setElement
as function ref, so when the element is rendered, it would call the setElement
and trigger the hook.
Note: the element
, handler
, and relativeOutsideElementSelector
. are dependencies of the hook's useEffect
. This means that you need to memoize the handler
to prevent the useEffect
from being trigger by the handler
being re-created on renders.
const hander = `useCallback(...)`; memoize the handler
const setElement = useOutsideElementClick(handler);
return (
<div ref={setElement} />
)
You can avoid the need to memoize the handler
by passing it via ref
to the useEffect
's callback:
export const useOutsideElementClick = (
handler: (target: EventTarget | null) => void,
relativeOutsideElementSelector?: string,
) => {
const [element, setElement] = useState<HTMLElement | null>(null);
const handlerRef = useRef(handler);
useEffect(() => { handlerRef.current = handler; }); // set the handlerRef
useLayoutEffect(() => {
if(!element) return; // skip if no element
const handleClickOutside = (event: MouseEvent) => {
if (element.contains(event.target as Node)) {
handler.current(event.target);
}
};
const modal = relativeOutsideElementSelector
? (document.querySelector(relativeOutsideElementSelector) as HTMLElement)
: (document as unknown as HTMLElement);
modal?.addEventListener('click', handleClickOutside);
return () => {
modal?.removeEventListener('click', handleClickOutside);
};
}, [element, relativeOutsideElementSelector]);
const return setElement;
};
Upvotes: 1