Prevent eventListener from firing multiple times

Viewed 1151

I am creating an HOC which adds the functionality to open hyperlinks externally if the input Component has any anchor tags.

Below is the HOC:

export const withExternalLink = ( Component, rootId = false ) => ( props ) => {
    function openLinksExternally( e, rootId ) {
        const rootHtml = e.target.closest( rootId );

        if ( ! rootHtml ) {
            return;
        }

        const anchor = e.target.closest( 'a' );

        if ( anchor ) {
            e.preventDefault();

            /**
             * Logic to open link externally.
             */
        }
    }

    useEffect( () => {
        document.addEventListener( 'click', ( e ) => openLinksExternally( e, rootId ) );
        return () => document.removeEventListener( 'click', ( e ) => openLinksExternally( e, rootId ) );
    }, [] );

    return <Component { ...props } />;
};

Usage:

const CardWithExternalLinks = withExternalLink( Card );

This works if the <CardWithExternalLinks /> is used once on a page. But if we use it n times, then useEffect() runs n times, which then adds event listener n times.

I was able to solve it by moving openLinksExternally() outside of withExternalLink(), this way there is just 1 reference to the function. But the problem is that I have to use an anonymous function to add event listener because I have to pass the rootId parameter to openLinksExternally().

Since I have used anonymous functions, a new event listener is registered for every useEffect(), and whenever an <a/> tag is clicked, openLinksExternally() runs n times.

Is there a way to better implement this so that the click runs once?

1 Answers

I fixed this with a closure, here is my solution:

export const withExternalLink = ( Component ) => ( rootId = false ) => {
    let isListenerAttached = false;

    return ( props ) => {
        function openLinksExternally( e, id ) {
            if ( ! id ) {
                return;
            }

            const rootHtml = e.target.closest( id );

            if ( ! rootHtml ) {
                return;
            }

            const anchor = e.target.closest( 'a' );

            if ( anchor ) {
                anchor.setAttribute( 'target', '_blank' );
            }
        }

        useEffect( () => {
            if ( ! isListenerAttached ) {
                document.addEventListener( 'click', ( e ) => openLinksExternally( e, rootId ) );
                isListenerAttached = true;
                return;
            }
            return () => document.removeEventListener( 'click', ( e ) => openLinksExternally( e, rootId ) );
        }, [] );

        return <Component { ...props } />;
    };
};
Related