Другое

Как обнаружить клики вне компонентов React

Узнайте лучшие методы обнаружения кликов вне компонентов React с помощью contains(), event.path и хуков. Руководство с примерами и советами по производительности.

Как определить, произошло ли событие клика вне компонента React?

Мне нужно способ обнаружить, случился ли клик вне компонента React. Это похоже на использование closest() в jQuery, чтобы проверить, является ли целевой элемент события одним из родителей DOM‑элемента компонента — если совпадение найдено, клик относится к дочерним элементам и не считается внешним.

В моём компоненте я хочу привязать обработчик клика к window. Когда обработчик срабатывает, мне нужно сравнить event.target с дочерними DOM‑элементами моего компонента.

Событие клика содержит свойства вроде path, которые, как кажется, содержат путь DOM, по которому прошёл событие. Я не уверен, что именно сравнивать и как лучше пройтись по этому пути, и меня интересует, есть ли уже готовая утилита для этой цели.

Какой лучший способ определить клики вне компонента React?

Самый надёжный способ обнаружить клик вне React‑компонента — добавить обработчик клика к document или window и использовать метод contains() для проверки, находится ли целевой элемент события внутри DOM‑элемента вашего компонента. Такой подход обеспечивает чистое, эффективное решение, совместимое с современными браузерами и соответствующее жизненному циклу компонентов React.

Содержание


Базовая реализация с contains()

Самый простой способ — добавить слушатель события клика к document и проверить, находится ли кликнутый элемент внутри элемента, к которому привязан ref.

javascript
import React, { useEffect, useRef } from 'react';

const ClickOutsideComponent = ({ children }) => {
  const ref = useRef(null);

  useEffect(() => {
    const handleClickOutside = (event) => {
      if (ref.current && !ref.current.contains(event.target)) {
        // Клик произошёл вне компонента
        console.log('Clicked outside!');
      }
    };

    // Добавляем слушатель события
    document.addEventListener('mousedown', handleClickOutside);

    return () => {
      // Очищаем слушатель при размонтировании
      document.removeEventListener('mousedown', handleClickOutside);
    };
  }, []); // Пустой массив зависимостей гарантирует, что это выполнится один раз

  return <div ref={ref}>{children}</div>;
};

Ключевые моменты

  • Используйте useRef, чтобы получить ссылку на DOM‑элемент компонента.
  • contains() возвращает true, если целевой элемент является потомком элемента ref.
  • Добавляйте слушатель в useEffect с правильной очисткой.
  • Используйте mousedown вместо click для лучшей производительности и согласованности.

Использование event.path и composedPath()

Современные браузеры предоставляют event.path и стандартизированный метод event.composedPath(), который возвращает массив элементов, через которые прошло событие.

javascript
useEffect(() => {
  const handleClickOutside = (event) => {
    const path = event.composedPath ? event.composedPath() : event.path || [];

    if (ref.current && !path.includes(ref.current)) {
      console.log('Clicked outside!');
    }
  };

  document.addEventListener('click', handleClickOutside);

  return () => {
    document.removeEventListener('click', handleClickOutside);
  };
}, []);

Преимущества этого подхода

  • Более полная информация о пути события.
  • Работает с Shadow DOM (используя composedPath()).
  • Не требуется ручная проверка DOM‑транзитивности.

Совместимость с браузерами

  • composedPath() — современный стандарт, работает во всех современных браузерах.
  • event.path — устаревшее свойство, которое может отсутствовать в некоторых браузерах.
  • При необходимости поддержки старых браузеров рассмотрите полифилы.

Создание переиспользуемого пользовательского хука

Для лучшей организации кода и переиспользуемости можно создать пользовательский хук:

javascript
import { useEffect, useRef } from 'react';

const useClickOutside = (callback) => {
  const ref = useRef(null);

  useEffect(() => {
    const handleClickOutside = (event) => {
      if (ref.current && !ref.current.contains(event.target)) {
        callback();
      }
    };

    document.addEventListener('mousedown', handleClickOutside);

    return () => {
      document.removeEventListener('mousedown', handleClickOutside);
    };
  }, [callback]);

  return ref;
};

// Использование в компоненте
const Dropdown = () => {
  const [isOpen, setIsOpen] = useState(false);
  const ref = useClickOutside(() => setIsOpen(false));

  return (
    <div ref={ref}>
      <button onClick={() => setIsOpen(!isOpen)}>
        Toggle Dropdown
      </button>
      {isOpen && (
        <div className="dropdown-content">
          {/* Dropdown content */}
        </div>
      )}
    </div>
  );
};

Этот пользовательский хук:

  • Инкапсулирует логику клика вне.
  • Предоставляет чистый API для любого компонента.
  • Автоматически обрабатывает очистку.
  • Легко тестируется.

Решения библиотек

Несколько библиотек предоставляют готовые решения для обнаружения клика вне:

1. React Hook Form

javascript
import { useClickOutside } from 'react-hook-form';

// Комбинируется с валидацией форм

2. Headless UI

javascript
import { Menu } from '@headlessui/react';

// Предоставляет встроенную обработку клика вне

3. Пользовательские утилитные библиотеки

javascript
// Библиотеки вроде 'click-outside-react'
import useClickOutside from 'click-outside-react';

Когда использовать библиотеки

  • Если нужны дополнительные функции, например, поддержка клавиатуры.
  • Для сложных иерархий компонентов.
  • Если требуется проверенный временем, поддерживаемый код.
  • Для команд с единообразным выбором библиотек.

Проблемы производительности

Преимущества делегирования событий

  • Один слушатель на document вместо множества.
  • Эффективное использование памяти.
  • Лучшее поведение при большом количестве компонентов.

Лучшие практики

  1. Используйте пассивные слушатели событий, когда это возможно, для улучшения прокрутки.
  2. Дебаунсите быстрые события при необходимости сложных взаимодействий.
  3. Правильно очищайте в useEffect, чтобы избежать утечек памяти.
  4. Рассмотрите типы событий – mousedown часто надёжнее, чем click.
  5. Избегайте встроенных обработчиков внутри компонента, которые могут конфликтовать.

Продвинутая оптимизация

javascript
// Делегирование событий с одним слушателем для нескольких компонентов
useEffect(() => {
  const handleGlobalClick = (event) => {
    document.querySelectorAll('.click-outside').forEach(element => {
      if (!element.contains(event.target)) {
        element.dispatchEvent(new CustomEvent('clickedOutside'));
      }
    });
  };

  document.addEventListener('mousedown', handleGlobalClick, { passive: true });

  return () => {
    document.removeEventListener('mousedown', handleGlobalClick);
  };
}, []);

Полный пример

Ниже приведена полная реализация выпадающего меню:

javascript
import React, { useState, useEffect, useRef } from 'react';

const useClickOutside = (callback) => {
  const ref = useRef(null);

  useEffect(() => {
    const handleClickOutside = (event) => {
      if (ref.current && !ref.current.contains(event.target)) {
        callback();
      }
    };

    document.addEventListener('mousedown', handleClickOutside);

    return () => {
      document.removeEventListener('mousedown', handleClickOutside);
    };
  }, [callback]);

  return ref;
};

const DropdownMenu = () => {
  const [isOpen, setIsOpen] = useState(false);
  const dropdownRef = useClickOutside(() => setIsOpen(false));

  const handleItemClick = (item) => {
    console.log('Selected:', item);
    setIsOpen(false);
  };

  return (
    <div className="dropdown-container">
      <button
        className="dropdown-toggle"
        onClick={() => setIsOpen(!isOpen)}
      >
        Options
      </button>

      {isOpen && (
        <div ref={dropdownRef} className="dropdown-menu">
          <div
            className="dropdown-item"
            onClick={() => handleItemClick('Option 1')}
          >
            Option 1
          </div>
          <div
            className="dropdown-item"
            onClick={() => handleItemClick('Option 2')}
          >
            Option 2
          </div>
          <div
            className="dropdown-item"
            onClick={() => handleItemClick('Option 3')}
          >
            Option 3
          </div>
        </div>
      )}
    </div>
  );
};

export default DropdownMenu;

CSS для стилизации

css
.dropdown-container {
  position: relative;
  display: inline-block;
}

.dropdown-toggle {
  padding: 8px 16px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.dropdown-menu {
  position: absolute;
  top: 100%;
  left: 0;
  background: white;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  z-index: 1000;
  min-width: 200px;
}

.dropdown-item {
  padding: 10px 15px;
  cursor: pointer;
  transition: background-color 0.2s;
}

.dropdown-item:hover {
  background-color: #f5f5f5;
}

Источники

  1. MDN Web Docs – EventTarget.contains()
  2. React Documentation – useRef and useEffect
  3. React Documentation – useEffect cleanup
  4. MDN Web Docs – Event.path
  5. MDN Web Docs – Event.composedPath()

Заключение

Ключевые выводы

  1. Метод contains() обеспечивает самый надёжный способ обнаружения клика вне React‑компонентов, проверяя, находится ли целевой элемент внутри DOM‑элемента компонента.
  2. Всегда очищайте слушатели событий в useEffect, чтобы избежать утечек памяти.
  3. Пользовательские хуки дают лучший подход для переиспользуемой, тестируемой функциональности клика вне.
  4. Рассмотрите использование mousedown вместо click для лучшей производительности и согласованности.
  5. Для сложных приложений оцените, может ли библиотечное решение предложить лучшую поддерживаемость.

Рекомендации

  • Начните с базового подхода contains() для большинства случаев.
  • Создайте пользовательский хук для лучшей организации кода и переиспользуемости.
  • Используйте решения библиотек только при необходимости дополнительных функций или сложных требований.
  • Всегда тестируйте реализацию на разных браузерах и устройствах.

Паттерн обнаружения клика вне является фундаментальным для создания интуитивно понятных пользовательских интерфейсов в React, и овладение им значительно улучшит ваши возможности проектирования компонентов.

Авторы
Проверено модерацией
Модерация