Last active
November 26, 2023 11:24
-
-
Save FaberVitale/bd691ef572ff4d8cf1f272e4f9d1a4ab to your computer and use it in GitHub Desktop.
useEventListener | React
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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 | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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