Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
import { useEffect, useRef, useCallback } from "react";
/**
* useClickOutside
* Chama `handler` quando um clique (ou toque) ocorre fora do elemento referenciado.
* Suporta um único ref ou um array de refs (ex: gatilho + popover).
*/
function useClickOutside<T extends HTMLElement = HTMLElement>(
handler: (event: MouseEvent | TouchEvent) => void,
refs?: React.RefObject<T | null>[]
): React.RefObject<T | null> {
const singleRef = useRef<T | null>(null);
const handlerRef = useRef(handler);
// Mantém o handler atualizado sem re-inscrição
useEffect(() => {
handlerRef.current = handler;
}, [handler]);
useEffect(() => {
const allRefs = refs ? refs : [singleRef];
const listener = (event: MouseEvent | TouchEvent) => {
// Verifica se o clique está dentro de algum dos elementos referenciados
const isInside = allRefs.some((ref) => {
return ref.current?.contains(event.target as Node);
});
if (!isInside) {
handlerRef.current(event);
}
};
// Usa mousedown/touchstart para resposta imediata
// (antes do evento click, que dispara no mouse up)
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [refs]);
return singleRef;
}Quando usar isso: Você tem um dropdown, modal, popover ou menu de contexto que deve fechar quando o usuário clicar em qualquer lugar fora dele.
"use client";
import { useState, useRef } from "react";
function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const ref = useClickOutside<HTMLDivElement>(() => setIsOpen(false));
return (
<div ref={ref} style={{ position: "relative", display: "inline-block" }}>
<button onClick={() => setIsOpen((o) => !o)}>
Menu {isOpen ? "▲" : "▼"}
</button>
{isOpen && (
<ul
style={{
position: "absolute",
top: "100%",
left: 0,
background: "#fff",
border: "1px solid #ccc",
listStyle: "none",
padding: 8,
margin: 0,
minWidth: 150,
boxShadow: "0 4px 12px rgba(0,0,0,0.1)",
}}
>
<li>Profile</li>
<li>Settings</li>
<li>Logout</li>
</ul>
)}
</div>
);
}
// Exemplo multi-ref: o botão de gatilho + o popover estão ambos "dentro"
function PopoverWithTrigger() {
const [isOpen, setIsOpen] = useState(false);
const triggerRef = useRef<HTMLButtonElement>(null);
const popoverRef = useRef<HTMLDivElement>(null);
useClickOutside(() => setIsOpen(false), [triggerRef, popoverRef]);
return (
<>
<button ref={triggerRef} onClick={() => setIsOpen((o) => !o)}>
Abrir Popover
</button>
{isOpen && (
<div
ref={popoverRef}
style={{
position: "absolute",
padding: 16,
background: "#fff",
border: "1px solid #e0e0e0",
borderRadius: 8,
boxShadow: "0 8px 24px rgba(0,0,0,0.12)",
}}
>
<p>Conteúdo do Popover</p>
<button onClick={() => setIsOpen(false)}>Fechar</button>
</div>
)}
</>
);
}O que isso demonstra:
mousedown dispara antes de click, proporcionando um comportamento de dismiss mais rápidomousedown vs click: Usar mousedown (e touchstart) permite que o dismiss ocorra antes que o clique seja concluído. Isso evita problemas onde um manipulador de clique no gatilho reabre um dropdown que acabou de fechar.contains: Node.contains() retorna true se o alvo do evento for o próprio elemento ou qualquer descendente, cobrindo filhos aninhados.| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
handler | (event: MouseEvent or TouchEvent) => void | - | Chamado quando um clique externo é detectado |
refs | RefObject<T>[] | - | Array opcional de refs para tratar como "dentro" |
| Retorna | RefObject<T> | - | Um ref para anexar (usado quando refs não é fornecido) |
Com tecla escape: Adicione o dismiss por teclado ao lado do click-outside:
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") handlerRef.current(e as any);
};
document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
}, []);Ignorar certos elementos: Aceite um predicado shouldIgnore para pular o dismiss para elementos específicos (por exemplo, toasts, modais que se empilham):
const listener = (event: MouseEvent) => {
if (shouldIgnore?.(event.target as HTMLElement)) return;
// ...lógica existente
};T extends HTMLElement tem como padrão HTMLElement, mas pode ser mais restrito: useClickOutside<HTMLDivElement>(...).MouseEvent | TouchEvent, pois ambos os tipos de evento são ouvidos.React.RefObject<T | null>[] para compatibilidade com useRef<T>(null).shouldIgnore ou verifique contra um nome de classe.mousedown dispara antes de click. Se o gatilho usar onClick para alternar a abertura, o handler de clique externo pode fechá-lo antes que o toggle o reabra. Correção: Envolva o contêiner e o gatilho no mesmo ref, ou use multi-ref.contains pode não funcionar corretamente com elementos SVG em alguns navegadores. Correção: Use event.target.closest() como uma verificação alternativa.touchstart junto com mousedown lida com isso.| Pacote | Nome do Hook | Notas |
|---|---|---|
usehooks-ts | useOnClickOutside | API semelhante, bem testado |
@uidotdev/usehooks | useClickAway | Implementação mínima |
ahooks | useClickAway | Suporta múltiplos refs nativamente |
react-aria | useInteractOutside | Acessível, lida com foco e ponteiro |
| Headless UI | Embutido | Dropdowns e popovers dispensam automaticamente |
mousedown dispara antes de click (que dispara no mouse up). Isso proporciona um comportamento de dismiss mais rápido e evita uma condição de corrida onde um manipulador de click no gatilho poderia reabrir um dropdown recém-fechado.
ref.current.contains(event.target) retorna true se o alvo do clique for o próprio elemento ou qualquer um de seus descendentes. Isso cobre cliques em filhos aninhados dentro do contêiner referenciado.
Funções de seta inline criam uma nova referência a cada renderização. Armazenar o handler em um ref evita re-inscrição dos ouvintes de eventos do documento a cada renderização, enquanto sempre chama a versão mais recente.
O conteúdo do portal está fora da árvore DOM do ref, mesmo que esteja logicamente "dentro" do componente. Use o padrão multi-ref, passando tanto o ref do gatilho quanto o ref do conteúdo do portal para o hook.
Use a variação shouldIgnore para pular o dismiss para elementos específicos:
const listener = (event: MouseEvent) => {
if ((event.target as HTMLElement).closest(".toast")) return;
handlerRef.current(event);
};Sim. O hook escuta tanto mousedown quanto touchstart, então toques em dispositivos móveis são tratados. Eventos de toque disparam antes de eventos de mouse em dispositivos de toque.
Adicione um ouvinte keydown para a tecla Escape dentro do mesmo efeito ou use useKeyboardShortcut:
useKeyboardShortcut("Escape", () => setIsOpen(false), {
enabled: isOpen,
});O genérico tem como padrão HTMLElement, mas pode ser mais restrito. Por exemplo, useClickOutside<HTMLDivElement>(handler) retorna um RefObject<HTMLDivElement | null>, fornecendo acesso seguro a tipos de propriedades específicas de div.
O handler recebe MouseEvent | TouchEvent, pois tanto mousedown quanto touchstart são ouvidos. Você pode restringir o tipo dentro do handler com verificações instanceof, se necessário.
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥