Як реалізувати Click Outside в React

💡 Усі статті, обговорення, новини про Front-end — в одному місці. Приєднуйтесь до Front-end спільноти!

Оскільки я майже сім років працюю з Angular, я вирішив писати такі статті для тих, хто переходить на React і стикається з проблемами реалізації різних речей у своїх апках.

Під час розробки компонента Sidebar на React у мене виникла необхідність реалізувати детектинг кліку за межами цього компонента, щоб закривати його після кожного кліку мишкою десь по екрану.

В Angular я робив для цього директиву:

@Directive({
  selector: '[clickOutside]'
})
export class ClickOutsideDirective {

  // Створюємо вихідне значення, яке буде емінуватися при кліку за межами елемента
  readonly clickOutside = output<void>();

  // Отримуємо посилання на елемент, до якого застосовується директива
  private readonly elementRef = inject(ElementRef)

  // Використовуємо HostListener для прослуховування події 'click' на документі
  @HostListener('document:click', ['$event.target'])
  public onClick(target) {

    // Перевіряємо, чи клікнув користувач всередині елемента
    const clickedInside = this.elementRef.nativeElement.contains(target);

    // Якщо клік був за межами елемента, емітимо подію
    if (!clickedInside) {
      this.clickOutside.emit();
    }
  }
}

Тут все зрозуміло, а тепер мені потрібно зробити щось подібне в React. Перше, що прийшло в голову — зробити кастомний хук.

Як він буде працювати? Ось його схема:

function MyComponent() {
  // Викликаємо кастомний хук useDetectClickOutside і передаємо конфігурацію
  const ref = useDetectClickOutside({

    // Функція, яка буде викликана при кліку за межами елемента або натисканні клавіші
    onTriggered: () => console.log('Clicked outside or pressed Escape'),

    // Список клавіш, на які буде реагувати хук (Escape та Enter)
    triggerKeys: ['Escape', 'Enter']
  });

  // Повертаємо елемент, до якого прив'язуємо хук
  return <div ref={ref}>Click outside me!</div>;
}

Тепер давайте перейдемо до його реалізацїї, перше що нам потрібно описати це пропси:

export interface Props {
  /**
   * Колбек-функція, яка викликається при детекції кліку за межами або натискання вказаної клавіші.
   */
  onTriggered: (e: Event) => void;

  /**
   * Якщо true, вимикає детекцію кліків.
   * @за замовчуванням false
   */
  disableClick?: boolean;

  /**
   * Якщо true, вимикає детекцію дотиків.
   * @за замовчуванням false
   */
  disableTouch?: boolean;

  /**
   * Якщо true, вимикає детекцію натискання клавіші.
   * @за замовчуванням false
   */
  disableKeys?: boolean;

  /**
   * Якщо true, викликає колбек на будь-яке натискання клавіші.
   * @за замовчуванням false
   */
  allowAnyKey?: boolean;

  /**
   * Масив конкретних клавіш, які повинні викликати колбек.
   * @за замовчуванням []
   */
  triggerKeys?: string[];
}

З пропсами наче все зрозуміло, тепер давайте напишиме наш хук:

export function useDetectClickOutside({
  onTriggered,
  disableClick = false,
  disableTouch = false,
  disableKeys = false,
  allowAnyKey = false,
  triggerKeys = [],
}: Props): React.RefObject<HTMLElement> {
 /**
  * useRef<HTMLElement | null>(null) використовується для створення посилання на DOM-елемент.
  * Це посилання дозволяє отримати доступ до конкретного елемента в компоненті, щоб виконати перевірку, чи був клік або дотик за межами цього елемента.
  */
  const ref = useRef<HTMLElement | null>(null);

  /**
   * Обробляє події натискання клавіші та викликає колбек, якщо умови виконуються.
   * useCallback використовується для мемоізації функцій в React.
   * Це дозволяє зберігати одну і ту ж функцію між рендерами, що запобігає її перевизначенню при кожному рендері компонента.
   */
  const handleKeyEvent = useCallback((e: KeyboardEvent) => {
    if (allowAnyKey || triggerKeys.includes(e.key) || e.key === 'Escape') {
      onTriggered(e);
    }
  }, [allowAnyKey, triggerKeys, onTriggered]);

  /**
   * Обробляє події кліку або дотику та викликає колбек, якщо подія сталася поза елементом, на який є посилання.
   */
  const handleClickOrTouch = useCallback((e: MouseEvent | TouchEvent) => {
    if (ref.current && !ref.current.contains(e.target as Node)) {
      onTriggered(e);
    }
  }, [onTriggered]);

  /**
  * useEffect — це хук, який дозволяє виконувати побічні ефекти в функціональних компонентах React.
  * Виконується після рендеру компонента, що дозволяє працювати з результатами рендеру чи змінювати стан компонента.
  */
  useEffect(() => {
    // Додаємо слухачів подій
    if (!disableClick) document.addEventListener('click', handleClickOrTouch);
    if (!disableTouch) document.addEventListener('touchstart', handleClickOrTouch);
    if (!disableKeys) document.addEventListener('keyup', handleKeyEvent);

    // Функція для очищення слухачів подій
    return () => {
      if (!disableClick) document.removeEventListener('click', handleClickOrTouch);
      if (!disableTouch) document.removeEventListener('touchstart', handleClickOrTouch);
      if (!disableKeys) document.removeEventListener('keyup', handleKeyEvent);
    };
  }, [disableClick, disableTouch, disableKeys, handleClickOrTouch, handleKeyEvent]);

  return ref;
}

Вітаю, тепер у вас є все необхідне для трекінгу кліків за межами елемента в React.

👍ПодобаєтьсяСподобалось1
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Загалом все виглядає добре, але я б зробив два зауваження.
1. Інтерфейс пропсів з назваою

Props

— це приблизно як змінна з назвою Data.
2. Забагато очевидних коментарів, які заважають читати код.

Підписатись на коментарі