Reactでドロップダウンを作る

## はじめに
React でドロップダウンを作成する際、実装方法に少し悩んだ部分があったため、備忘録として残しておこうと思います。

## 完成イメージ
![画面収録 2024-02-26 0.25.23.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/874634/c271b0a0-6808-dbcf-aa3f-6eb5dee149de.gif)

## 実装コード

```react:useDropdown.tsx
import { useState, useRef, useEffect } from "react";
import { Fruit } from "./Dropdown";

export const useDropdown = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [selectedFruit, setSelectedFruit] = useState<Fruit>();

  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!isOpen) return;
    const onClick = (e: Event) => {
      if (containerRef.current?.contains(e.target as Node)) return;
      setIsOpen(false);
    };
    document.addEventListener("mousedown", onClick);
    return () => {
      document.removeEventListener("mousedown", onClick);
    };
  }, [isOpen, setIsOpen]);

  const onClickDropdown = () => {
    setIsOpen((flag) => !flag);
  };

  const onClickItem = (item: Fruit) => {
    setSelectedFruit(item);
    setIsOpen(false);
  };
  return {
    isOpen,
    onClickDropdown,
    onClickItem,
    selectedFruit,
    containerRef
  };
};

```

```react:index.tsx
import { useDropdown } from "./useDropdown";
import { Presentation } from "./Dropdown";

export function Dropdown() {
  const props = useDropdown();
  return <Presentation {...props} />;
}
```

```react:Dropdown.tsx
export type Fruit = "りんご" | "バナナ" | "メロン" | "いちご";
type Props = {
  isOpen: boolean;
  onClickDropdown: () => void;
  onClickItem: (fruit: Fruit) => void;
  selectedFruit: Fruit;
  containerRef: React.RefObject<HTMLDivElement>;
};

const fruits: Fruit[] = ["りんご", "バナナ", "メロン", "いちご"];

export const Presentation: React.FC<Props> = ({
  isOpen,
  onClickDropdown,
  onClickItem,
  selectedFruit,
  containerRef
}) => (
  <div className="dropdown" ref={containerRef}>
    <button onClick={onClickDropdown} className="dropdown__button">
      <span>好きなくだもの</span>
      <span>{isOpen ? "▲" : "▼"}</span>
    </button>
    {isOpen ? (
      <div className="dropdown__wrap">
        <ul className="dropdown__item-list">
          {fruits.map((fruit) => (
            <li
              key={fruit}
              className="dropdown__item"
              onClick={() => onClickItem(fruit)}
            >
              {fruit}
            </li>
          ))}
        </ul>
      </div>
    ) : null}
    {selectedFruit && !isOpen && (
      <div className="dropdown__selected-item">
        選択しているフルーツ：{selectedFruit}
      </div>
    )}
  </div>
);
```

## 軽く解説
ドロップダウンをクリックしたときにアイテムリストを表示したり、選択したら閉じる、といった制御は useState（`isOpen`）を使っています。

また、アイテムリストが表示されているときに、ドロップダウンの範囲外をクリックした場合、何もせず閉じるようにするため、useRef を使って制御しています。
```react
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
  // アイテムリストが表示されていない場合はこの後の処理は必要ないためreturn
  if (!isOpen) return;
  const onClick = (e: Event) => {
    // クリックした場所がドロップダウンコンポーネントの要素内のときはreturn
    if (containerRef.current?.contains(e.target as Node)) return;
    // クリックした場所がドロップダウンコンポーネントの要素外の時は何もせずアイテムリストを非表示にする
    setIsOpen(false);
  };
  document.addEventListener("mousedown", onClick);
  return () => {
    document.removeEventListener("mousedown", onClick);
  };
}, [isOpen, setIsOpen]);
  ```

## さいごに
解説に書いた「アイテムリストが表示されているときに、ドロップダウンの範囲外をクリックした場合、何もせず閉じるようにする」この部分の実装に少し悩みましたが、useRef と addEventListener() を組み合わせて使うことで実装することができました。