Skip to content

Instantly share code, notes, and snippets.

@FaberVitale
Last active November 26, 2023 11:24
Show Gist options
  • Save FaberVitale/bd691ef572ff4d8cf1f272e4f9d1a4ab to your computer and use it in GitHub Desktop.
Save FaberVitale/bd691ef572ff4d8cf1f272e4f9d1a4ab to your computer and use it in GitHub Desktop.
useEventListener | React
/* eslint-disable prefer-spread, consistent-return */
import { useEffect, useLayoutEffect, useRef } from 'react'
type Nullable<T> = T | null
type EventHandler<T, E> = (this: T, evt: E) => any
export type GetAddListenerOptions = {
(eventType: string): AddEventListenerOptions | boolean | undefined
}
/**
* Registers an event listener `handler` of type `eventType` to `target`.
*
* This hooks update the subcription only when `eventType` or target changes.
*
* The event handler and `getAddListenerOptions` are managed in react refs to avoid stale closures.
*
* @param eventType e.g. 'load'
* @param handler an event listener or null to temporary remove the event listener
* @param target an EventTarget or null or undefined to remove event subscription.
* @param getAddListenerOptions optional producer of [eventListener options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#parameters).
*
* ### Simple Usage
*
* ```ts
* useEventListener('focus', () => { console.log('FOCUS')}, typeof document !== 'undefined' ? document.body : null);
* ```
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
*/
export function useEventListener<
Evt extends Event,
T extends EventTarget = EventTarget
>(
eventType: string,
handler: Nullable<EventHandler<void, Evt>>,
target: Nullable<T> | undefined,
getAddListenerOptions?: GetAddListenerOptions
): void {
const actualTarget: Nullable<T> = target ?? null
const hasHandler = typeof handler === 'function'
const handlerRef = useRef(handler)
const getOptionsRef = useRef(getAddListenerOptions)
// Prevent stale closures
useLayoutEffect(() => {
handlerRef.current = handler
getOptionsRef.current = getAddListenerOptions
})
useEffect(() => {
function eventHandlerAdapter(this: T, evt: Evt) {
if (typeof handlerRef.current === 'function') {
handlerRef.current(evt)
}
}
if (hasHandler && actualTarget) {
const options = getOptionsRef.current?.(eventType)
const addEventListenerArgs:
| [string, EventListener, AddEventListenerOptions | boolean]
| [string, EventListener] =
options != null
? [eventType, eventHandlerAdapter as EventListener, options]
: [eventType, eventHandlerAdapter as EventListener]
actualTarget.addEventListener.apply(actualTarget, addEventListenerArgs)
return () => {
actualTarget.removeEventListener.apply(
actualTarget,
addEventListenerArgs
)
}
}
}, [eventType, actualTarget, hasHandler])
}
/**
* Registers an event listener `handler` of type `eventType` to window.
* @param eventType e.g. 'load'
* @param handler an event listener or null to temporary remove the event
* @param getAddListenerOptions optional producer of [eventListener options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#parameters).
*
* ### Simple Usage
*
* ```ts
* useWindowEvent("message", (evt: MessageEvent): void => {
* if (evt.origin !== 'https://www.example-origin.com') {
* return;
* }
*
* // Manage event.data somehow
* });
* ```
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
*/
export function useWindowEvent<K extends keyof WindowEventMap>(
type: K,
listener: Nullable<EventHandler<void, WindowEventMap[K]>>,
getAddListenerOptions?: GetAddListenerOptions
): void
export function useWindowEvent<Evt extends Event>(
eventType: string,
handler: Nullable<EventHandler<void, Evt>>,
getAddListenerOptions?: GetAddListenerOptions
): void
export function useWindowEvent<Evt extends Event>(
eventType: string,
handler: Nullable<EventHandler<void, Evt>>,
getAddListenerOptions?: GetAddListenerOptions
): void {
useEventListener(
eventType,
handler,
typeof window !== 'undefined' ? window : null,
getAddListenerOptions
)
}
import { useWindowEvent } from './useEventListener'
import type { MutableRefObject } from 'react'
export function useOnClickOutside<T extends HTMLElement = HTMLElement>(
ref: MutableRefObject<T | null>,
handler: ((evt: MouseEvent) => any) | null | undefined
): void {
useWindowEvent('click', (event) => {
const el = ref?.current
// Do nothing if clicking ref's element or descendent elements
if (!el || !handler || el.contains(event.target as Node)) {
return
}
handler(event)
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment