はじめに
これまでドロップダウンメニューが必要な時は React Bootstrap を用いていたのですが、自力でドロップダウンメニューを作る必要があったので、そのときの試行錯誤をメモとして残します。 ドロップダウンメニューが満たしてほしい仕様は以下の通りです。
- ボタンクリックでメニューを表示/非表示
 - メニュー外をクリックしてもメニューが非表示になる
 
Step 1
まずは上記の一つ目の条件のみを満たす、基本的なドロップダウンメニューを作ります
      
    
  
        
const Dropdown1 = () => {
  const [isOpenMenu, setIsOpenMenu] = React.useState(false);
  const handleClick = (text) => () => {
    alert(text);
    setIsOpenMenu(false);
  };
  return (
    <div className="menu-container" onClick={() => setIsOpenMenu(!isOpenMenu)}>
      <div className="menuButton">Menu 1</div>
      <ul className="menu" hidden={!isOpenMenu}>
        <li className="item" onClick={handleClick("a")}>
          {" "}
          a{" "}
        </li>
        <li className="item" onClick={handleClick("b")}>
          {" "}
          b{" "}
        </li>
        <li className="item" onClick={handleClick("c")}>
          {" "}
          c{" "}
        </li>
      </ul>
    </div>
  );
};Step 2
この Step 1 のメニューではメニューを表示した状態でメニュー外をクリックしてもメニューが閉じません。
なのでuseRefとuseEffectを使って、ボタンをクリックするとドロップダウンメニューにフォーカスが来るようにし、フォーカスが外れた時にonBlurイベントにメニューを閉じる関数を登録します。
const Dropdown2 = () => {
  const [isOpenMenu, setIsOpenMenu] = React.useState(false);
  const menuRef = React.useRef(null);
  React.useEffect(() => {
    isOpenMenu && menuRef.current.focus();
  }, [isOpenMenu]);
  const handleClick = (text) => () => {
    alert(text);
  };
  return (
    <div
      className="menu-container"
      onClick={() => setIsOpenMenu(!isOpenMenu)}
      ref={menuRef}
      onBlur={() => setIsOpenMenu(false)}
      tabIndex={0}
    >
      <div className="menuButton">Menu 2</div>
      <ul className="menu" hidden={!isOpenMenu}>
        <li className="item" onClick={handleClick("a")}>
          {" "}
          a{" "}
        </li>
        <li className="item" onClick={handleClick("b")}>
          {" "}
          b{" "}
        </li>
        <li className="item" onClick={handleClick("c")}>
          {" "}
          c{" "}
        </li>
      </ul>
    </div>
  );
};Step 3
Step 2 のドロップダウンメニューにメニューアイテム間のセパレータとサブメニューを同様に追加してみます。
const Dropdown3 = () => {
  const [isOpenMenu, setIsOpenMenu] = React.useState(false);
  const menuRef = React.useRef(null);
  React.useEffect(() => {
    isOpenMenu && menuRef.current.focus();
  }, [isOpenMenu]);
  const handleClick = (text) => () => {
    alert(text);
  };
  return (
    <div
      className="menu-container"
      onClick={() => setIsOpenMenu(!isOpenMenu)}
      ref={menuRef}
      onBlur={() => setIsOpenMenu(false)}
      tabIndex={0}
    >
      <div className="menuButton">Menu 3</div>
      <ul className="menu" hidden={!isOpenMenu}>
        <li className="item" onClick={handleClick("a")}>
          {" "}
          a{" "}
        </li>
        <li className="separator"></li>
        <li className="item" onClick={handleClick("b")}>
          {" "}
          b{" "}
        </li>
        <li className="item">
          {" "}
          c<span>▶</span>
          <ul className="submenu">
            <li className="item" onClick={handleClick("c-1")}>
              {" "}
              c-1{" "}
            </li>
            <li className="item" onClick={handleClick("c-2")}>
              {" "}
              c-2{" "}
            </li>
          </ul>
        </li>
      </ul>
    </div>
  );
};一見動作するのですが、少しおかしい挙動が見られます。
というのは、セパレータとサブメニュー「c」をクリックしてもメニューが閉じてしまうのです。
これは親要素へクリックイベントが伝播してしまい、div要素のonClick関数が実行されてしまうからです。(恐らく)
Step 4
そこでドロップダウンメニュー全体の親であるulとサブメニューの親であるliに(e) => e.stopPropagation()を追加したものが以下です。
これにより先述のおかしい動作は見られなくなります。
const Dropdown4 = () => {
  const [isOpenMenu, setIsOpenMenu] = React.useState(false);
  const menuRef = React.useRef(null);
  React.useEffect(() => {
    isOpenMenu && menuRef.current.focus();
  }, [isOpenMenu]);
  const handleClick = (text) => () => {
    alert(text);
  };
  return (
    <div
      className="menu-container"
      onClick={() => setIsOpenMenu(!isOpenMenu)}
      ref={menuRef}
      onBlur={() => setIsOpenMenu(false)}
      tabIndex={0}
    >
      <div className="menuButton">Menu 4</div>
      <ul
        className="menu"
        hidden={!isOpenMenu}
        onClick={(e) => e.stopPropagation()}
      >
        <li className="item" onClick={handleClick("a")}>
          {" "}
          a{" "}
        </li>
        <li className="separator"></li>
        <li className="item" onClick={handleClick("b")}>
          {" "}
          b{" "}
        </li>
        <li className="item" onClick={(e) => e.stopPropagation()}>
          {" "}
          c<span>▶</span>
          <ul className="submenu">
            <li className="item" onClick={handleClick("c-1")}>
              {" "}
              c-1{" "}
            </li>
            <li className="item" onClick={handleClick("c-2")}>
              {" "}
              c-2{" "}
            </li>
          </ul>
        </li>
      </ul>
    </div>
  );
};以上
です。
Qiita にも書きました
React+Hook で作るメニュー外クリックで閉じるドロップダウンメニュー