React closing a dropdown when click outside

Viewed 21729

Building a custom dropdown in React and came across a problem when trying to close it when clicking outside.
So i created generic HOC so i can use it for other ocasions as well.

The problem i seem to have is i don't know hwo to pass the ref from the hoc to the component.

I've been looking into forwardRef and other tones of examples, but i can't figure it out.
Any idea if it's possible and how i could do it?

Codesandbox Demo

import React, { useState, useRef, useEffect } from "react";

export default function withClickOutside(WrappedComponent) {
  const Component = (props) => {
    const [open, setOpen] = useState(false);

    const ref = useRef();

    useEffect(() => {
      const handleClickOutside = (event) => {
        if (!ref?.current?.contains(event.target)) {
          setOpen(false);
        }
      };
      document.addEventListener("mousedown", handleClickOutside);
    }, [ref]);

    return <WrappedComponent {...props} open={open} setOpen={setOpen} ref={ref}/>;
  };

  return Component;
}

3 Answers

Simple follow 5 steps to close menu/ change state of any element when clicked outside of that element using React Hook

Step 1 : import useRef and define a variable

import {useRef} from "react"

const catMenu = useRef(null)

Step 2 : use React Hook for state chnage (useState)

  const [openSlide, setopenSlide] = useState(""); 

Step 3 : Define a javascript function to set state

   const closeOpenMenus = (e)=>{
    if(catMenu.current && openSlide && !catMenu.current.contains(e.target)){
      setopenSlide(false)
    }
}

Step 4 : Add a event listener on when mouse is down anywhere in document

 document.addEventListener('mousedown',closeOpenMenus)

Step 5 : Select target element (ref={variable_name})

<div ref={catMenu}>... </div>

You need to handle the ref passed so that it actually attaches to something:

const Home = forwardRef(({ open, setOpen }, ref) => {
  console.log(open);

  return (
    <section ref={ref}>
      <h1>Feels good to be home!</h1>

      <button className="secondary" onClick={() => setOpen(!open)}>
        Dropdown Toggle
      </button>

      {open && (
        <ul>
          <li>One</li>
          <li>Two</li>
        </ul>
      )}
    </section>
  );
});

Edit React withClickOutside (forked)

It is a little diff. from your approach, but this hook can used to wrap your button and menu.

/**
 * This Hook can be used for detecting clicks outside the Opened Menu
 */
function clickOutside(ref, onClickOutside) {
  useEffect(() => {
    /**
     * Invoke Function onClick outside of element
     */
    function handleClickOutside(event) {
      if (ref.current && !ref.current.contains(event.target)) {
        onClickOutside();
      }
    }
    // Bind
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      // dispose
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [ref, onClickOutside]);
}

Implemented with your code:

const Home = () => {
  let [open, setOpen] = useState(false);
  const wrapperRef = useRef("menu");
  outsideClickAlert(wrapperRef, () => {
    setOpen(false);
  });

  return (
    <section>
      <h1>Feels good to be home!</h1>
      <div ref={wrapperRef}>
        <button className="secondary" onClick={() => setOpen(!open)}>
          Dropdown Toggle
        </button>

        {open && (
          <ul>
            <li>One</li>
            <li>Two</li>
          </ul>
        )}
      </div>
    </section>
  );
};

Related