useDocumentEvent()

A React hook for performantly attaching event listeners to the document

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.

js
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.

js
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:

js
// 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 */});
Share this lovely stuff with your besties!

A newsletter for front-end web developers

Stay up-to-date with my latest articles, experiments, tools, and much more!

Issued monthly (or so). No spam. Unsubscribe any time.