1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

Reactを使っている人は、たぶん一度はこのコードを書いたことがあると思います。

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

Stack Overflowからコピーして、動いたから次に進む。
来週、モーダルのために同じコードをまた書く。
その次の週、ドロップダウンのために。
また次の週、ツールチップのために。

覚えがありますか?

だから、@kitsunechaos/use-outside-click を作りました。


問題

「要素の外をクリックしたか」を検知するのは、よく使うUIのパターンです:

  • ドロップダウンを閉じる
  • モーダルの外をクリックしたら閉じる
  • ポップオーバーやツールチップを隠す
  • モバイルのナビゲーションを閉じる

でも毎回、同じボイラープレートを書いています。
または、2018年に作られた古い6KBのライブラリを使っています。

もっといい方法があるはずです。


紹介:@kitsunechaos/use-outside-click

シンプルなReact Hookです。要素の外のクリックを検知します。それだけです。

npm install @kitsunechaos/use-outside-click

使い方

import { useRef } from "react";
import useOutsideClick from "@kitsunechaos/use-outside-click";

function Dropdown() {
  const ref = useRef(null);
  const [open, setOpen] = useState(false);

  useOutsideClick(ref, () => setOpen(false));

  return (
    <div ref={ref}>
      <button onClick={() => setOpen(true)}>Open</button>
      {open && <ul>{/* ドロップダウンのアイテム */}</ul>}
    </div>
  );
}

importは1つ。refは1つ。callbackは1つ。終わり。


このHookの特徴

✅ 依存関係ゼロ

余分なパッケージはありません。node_modulesが増えません。

✅ TypeScript対応

最初からTypeScriptで書いています。型推論が完全に動きます。@types/は不要です。

useOutsideClick(ref: RefObject<HTMLElement>, callback: () => void): void

✅ SSR対応

Next.jsやRemixでも問題なく動きます。window is not definedエラーは出ません。

✅ タッチ・マウス両対応

mousedowntouchstartの両方に対応しています。スマホでも動きます。

✅ サイズが小さい

gzip後、400バイト以下です。ユーザーは気づきません。

✅ 複数のRefに対応

複数の要素を同時に監視できます。


実際の例:ドロップダウンメニュー

import { useRef, useState } from "react";
import useOutsideClick from "@kitsunechaos/use-outside-click";

export function DropdownMenu() {
  const ref = useRef<HTMLDivElement>(null);
  const [isOpen, setIsOpen] = useState(false);

  useOutsideClick(ref, () => setIsOpen(false));

  return (
    <div ref={ref} style={{ position: "relative", display: "inline-block" }}>
      <button onClick={() => setIsOpen((prev) => !prev)}>
        Options ▾
      </button>
      {isOpen && (
        <ul style={{ position: "absolute", background: "white", border: "1px solid #eee" }}>
          <li>Edit</li>
          <li>Duplicate</li>
          <li>Delete</li>
        </ul>
      )}
    </div>
  );
}

removeEventListenerの書き忘れはもうありません。コードがきれいになります。


Before / After

Before(毎回このボイラープレート):

// 10行以上、毎回書く
useEffect(() => {
  function handleClickOutside(event) {
    if (ref.current && !ref.current.contains(event.target)) {
      onClose();
    }
  }
  document.addEventListener("mousedown", handleClickOutside);
  return () => {
    document.removeEventListener("mousedown", handleClickOutside);
  };
}, []);

After(1行):

useOutsideClick(ref, onClose);

インストール

# npm
npm install @kitsunechaos/use-outside-click

# yarn
yarn add @kitsunechaos/use-outside-click

# pnpm
pnpm add @kitsunechaos/use-outside-click

リンク

このHookが役に立ったら、GitHubに ⭐ をつけてもらえると嬉しいです。他の開発者が見つけやすくなります。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?