This hook lets you attach one or more event listeners to the document without having to wrap your handler in useCallback
.
The hook automatically unbinds the listeners when the component unmounts.
const useDocumentEvent = (events, callback) => {
const ref = useRef(callback);
ref.current = callback; // Always point to the most recent callback
useEffect(() => {
const eventsArray = events.split(' ');
const handler = e => ref.current(e);
eventsArray.forEach(event => document.addEventListener(event, handler));
return () => eventsArray.forEach(event => document.removeEventListener(event, handler));
}, [events, ref]);
};
#Re-binding cycles in useEffect
In React, attaching event listeners to the document
or window
is done via useEffect
because:
- You don't want to attach a new listener every render
- You need to cleanup (remove the event listener) when your component unmounts
However, if your handler is depending on external values, you need to add those to the useEffect
dependency
list, which will cause it to remove the old listener and add the new listener every time that dependency changes.
I refer to this phenomenon as "re-binding cycles".
By maintaining a ref
that always points to the most recent handler version, we can avoid this re-binding cycle.
Instead of passing callback
to the dependency list, we pass the ref, which will never change.
#Implementation with useValue()
We can somewhat simplify the above implementation by using useValue() to maintain the most recent handler version.
const useDocumentEvent = (events, callback) => {
const { get } = useValue(callback);
useEffect(() => {
const eventsArray = events.split(' ');
const handler = e => get()(e);
eventsArray.forEach(event => document.addEventListener(event, handler));
return () => eventsArray.forEach(event => document.removeEventListener(event, handler));
}, [events, get]);
};
#Usage examples:
// Attach an event listener to the document
useDocumentEvent('resize', e => {/* Do something */});
// Attach multiple event listeners to the document
useDocumentEvent('resize scroll', e => {/* Do something */});