useOutsideClick

Хук useOutsideClick из библиотеки Chakra UI предназначен для обработки кликов вне определённого DOM-элемента. Он используется в ситуациях, когда необходимо реагировать на взаимодействие пользователя за пределами компонента: закрывать выпадающие меню, модальные окна, всплывающие панели, тултипы или кастомные контекстные элементы интерфейса.

Типичный пользовательский сценарий:

  • открыт выпадающий список;
  • пользователь нажимает на любую область страницы вне списка;
  • компонент автоматически закрывается.

Реализация подобного поведения вручную требует установки глобального обработчика событий mousedown или click, проверки event.target, а также корректной очистки слушателей. useOutsideClick инкапсулирует эту логику и предоставляет декларативный API.

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


Импорт хука

Хук импортируется непосредственно из пакета Chakra UI:

import { useOutsideClick } from "@chakra-ui/react"

Базовый принцип работы

Механизм работы состоит из нескольких этапов:

  1. Определяется DOM-элемент, внутри которого клики считаются допустимыми.

  2. Устанавливается глобальный обработчик событий на документ.

  3. При возникновении события происходит проверка:

    • принадлежит ли event.target указанному элементу.
  4. Если клик произошёл вне области элемента, вызывается переданный обработчик.

Основная идея заключается в использовании ref, указывающего на DOM-узел.


Базовый пример использования

import { Box, Button, useOutsideClick } from "@chakra-ui/react"
import { useRef, useState } from "react"

function Dropdown() {
  const ref = useRef()
  const [isOpen, setIsOpen] = useState(false)

  useOutsideClick({
    ref: ref,
    handler: () => setIsOpen(false),
  })

  return (
    <Box>
      <Button onCl ick={() => setIsOpen(!isOpen)}>
        Toggle menu
      </Button>

      {isOpen && (
        <Box ref={ref} p={4} bg="gray.100">
          Dropdown content
        </Box>
      )}
    </Box>
  )
}

Структура взаимодействия:

  1. создаётся ref;
  2. ref передаётся в элемент меню;
  3. useOutsideClick получает этот ref;
  4. если клик происходит вне ref.current, вызывается handler.

Параметры хука

useOutsideClick принимает объект конфигурации.

ref

Ссылка на DOM-элемент, внутри которого клики не считаются внешними.

Тип:

RefObject<HTMLElement>

Пример:

const ref = useRef()

handler

Функция, вызываемая при клике вне элемента.

useOutsideClick({
  ref,
  handler: () => {
    console.log("Clicked outside")
  },
})

Обычно обработчик изменяет состояние интерфейса — например, закрывает панель.


enabled (опционально)

Позволяет включать и отключать обработчик.

useOutsideClick({
  ref,
  handler: closeMenu,
  enabled: isOpen,
})

Когда enabled равен false, обработчик событий не активируется.

Это важно для оптимизации, особенно при большом количестве компонентов.


Поведение при вложенных элементах

Если пользователь кликает по дочернему элементу внутри ref, событие не считается внешним.

Пример структуры DOM:

Box (ref)
 ├─ Button
 ├─ Text
 └─ Icon

Любой клик внутри этой структуры не вызовет handler.

Проверка происходит через метод:

element.contains(event.target)

Типичный паттерн: закрытие выпадающего меню

function MenuExample() {
  const ref = useRef()
  const [open, setOpen] = useState(false)

  useOutsideClick({
    ref,
    handler: () => setOpen(false),
    enabled: open,
  })

  return (
    <Box>
      <Button onCl ick={() => setOpen(true)}>Open</Button>

      {open && (
        <Box ref={ref} bg="white" shadow="md" p={4}>
          Menu item 1
          Menu item 2
        </Box>
      )}
    </Box>
  )
}

Логика работы:

  1. меню открывается кнопкой;
  2. пользователь может взаимодействовать с меню;
  3. клик вне области меню закрывает его.

Использование с модальными элементами

useOutsideClick часто используется при создании кастомных модальных окон.

function Modal() {
  const ref = useRef()

  useOutsideClick({
    ref,
    handler: () => closeModal(),
  })

  return (
    <Box className="overlay">
      <Box ref={ref} className="modal">
        Modal content
      </Box>
    </Box>
  )
}

В такой архитектуре:

  • overlay покрывает весь экран;
  • modal — центральный контейнер;
  • клик вне контейнера закрывает окно.

Работа с несколькими интерактивными зонами

Иногда требуется исключить несколько областей из обработки клика. В таких случаях обычно используется общий контейнер.

<Box ref={ref}>
  <Input />
  <Button />
  <Box>Dropdown</Box>
</Box>

Все вложенные элементы будут считаться внутренними.


Поведение при порталах

Компоненты Chakra UI могут рендериться через портал (Portal), что перемещает DOM-узел за пределы текущей иерархии.

Если элемент отображается в портале, важно учитывать:

  • ref должен указывать на реальный DOM-элемент портала
  • иначе проверка contains() не сработает корректно.

Обработка событий

Хук обычно подписывается на событие:

mousedown

или

pointerdown

Использование mousedown имеет преимущество:

  • событие срабатывает раньше, чем click;
  • можно предотвратить нежелательные визуальные эффекты.

Событийная модель:

mousedown → mouseup → click

Очистка обработчиков

useOutsideClick автоматически управляет жизненным циклом слушателей событий:

  1. добавляет обработчик при монтировании;
  2. удаляет его при размонтировании;
  3. пересоздаёт при изменении зависимостей.

Пример упрощённой внутренней логики:

useEffect(() => {
  function handle(event) {
    if (!ref.current?.contains(event.target)) {
      handler(event)
    }
  }

  document.addEventListener("mousedown", handle)

  return () => {
    document.removeEventListener("mousedown", handle)
  }
}, [ref, handler])

Работа с динамическими ссылками (ref)

Важно, чтобы ref всегда указывал на актуальный DOM-элемент.

Некорректный пример:

const ref = useRef(null)

но ref не передан в компонент:

<Box>

Правильный вариант:

<Box ref={ref}>

Сценарий: выпадающая панель поиска

function SearchPanel() {
  const ref = useRef()
  const [active, setActive] = useState(false)

  useOutsideClick({
    ref,
    handler: () => setActive(false),
  })

  return (
    <Box ref={ref}>
      <Input
        onFo cus={() => setActive(true)}
        placeholder="Search..."
      />

      {active && (
        <Box bg="white" shadow="sm">
          Search results
        </Box>
      )}
    </Box>
  )
}

Особенности поведения:

  • поле активирует панель;
  • клики вне области закрывают результаты.

Использование вместе с состоянием интерфейса

useOutsideClick обычно комбинируется с useState.

Пример:

const [isOpen, setIsOpen] = useState(false)

и обработчиком:

handler: () => setIsOpen(false)

Таким образом обеспечивается декларативное управление интерфейсом.


Производительность

Хук реализован таким образом, чтобы не создавать лишних слушателей.

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

1. Использовать enabled

enabled: isOpen

Это предотвращает регистрацию обработчика, когда компонент закрыт.

2. Избегать лишних пересозданий handler

Желательно использовать useCallback.

const close = useCallback(() => {
  setOpen(false)
}, [])

Частые ошибки

Отсутствие ref

useOutsideClick({
  ref,
  handler: close,
})

но ref не передан элементу.


Указание неправильного DOM-узла

Если ref указывает на родительский контейнер страницы, почти все клики будут считаться внутренними.


Игнорирование порталов

Компоненты, отображающиеся через портал, могут не входить в область contains().


Когда использовать useOutsideClick

Хук применяется в следующих интерфейсных элементах:

  • выпадающие меню
  • контекстные меню
  • панели настроек
  • всплывающие карточки
  • кастомные селекты
  • панели фильтров
  • модальные элементы без overlay
  • автодополнение поиска

Во всех этих случаях требуется единая логика:

клик вне элемента → изменение состояния интерфейса.


Сравнение с ручной реализацией

Ручной подход:

document.addEventListener("mousedown", handler)

Проблемы такого подхода:

  • необходимость ручной очистки
  • сложность управления зависимостями
  • риск утечек памяти
  • дублирование логики

useOutsideClick решает эти задачи, предоставляя декларативный интерфейс.


Комбинация с другими хуками Chakra UI

Хук часто используется вместе с:

  • useDisclosure
  • useBoolean
  • useControllableState

Пример:

const { isOpen, onOpen, onClose } = useDisclosure()

useOutsideClick({
  ref,
  handler: onClose,
})

Это формирует стандартный паттерн управления состоянием интерфейса.


Архитектурная роль в интерфейсах

useOutsideClick относится к категории interaction hooks — хуков, управляющих поведением пользовательских взаимодействий.

В архитектуре интерфейсов он решает задачу:

детектирование внешнего взаимодействия с компонентом.

Такая абстракция делает сложные интерактивные элементы значительно проще в реализации и поддержке.