Search code examples
javascriptreactjsreact-hooksuse-effect

React Hooks: useEffect for modal event listener


I have a modal dialog that I want to close if the user clicks outside of the modal. I have written the following useEffect code but I run into following issue:

The modal dialog contains a number of children (React Nodes) and those children might change (e.g. the user deletes an entry of a list). Those interactions trigger my onClick method but as the clicked list item has been removed from the modal, the modal closes even though the click was within the modal.

I thought adding [ children ] at the second parameter for useEffect would cleanup the old effect event listener quick enough that the method does not run again but this is not the case.

I handled the same issue in a class component with a ignoreNextClick-state but there must be a cleaner solution, right?

    useEffect( () => {
        const onClick = ( event ) => {
            const menu = document.getElementById( 'singleton-modal' );
            if ( !menu ) return;

            // do not close menu if user clicked inside
            const targetInMenu = menu.contains( event.target );
            const targetIsMenu = menu === event.target;
            if ( targetInMenu || targetIsMenu ) return;

            onCloseModal();
        };

        window.addEventListener( 'click', onClick, false );

        return () => window.removeEventListener( 'click', onClick, false );
    }, [ children ] );

Solution

  • I found a solution that does not require any sort of storing old props.

    The useEffect call looks like this:

    useEffect( () => {
            const onClickOutside = () => onCloseModal();
            window.addEventListener( 'click', onClickOutside, false );
            return () => window.removeEventListener( 'click', onClickOutside );
        }, [] );
    

    Adding the following click listener to the modal directly will stop the window click-listener from being called if the user clicked inside the modal.

    <div
        className={`modal ${ classes }`}
        onClick={event => event.stopPropagation()}
        role="presentation"
    >
       {children}
    </div>`
    

    I also added the role presentation to make the modal more accessible and aria-conform.