Skip to content
Browser

useEventListener

Registers an event listener with addEventListener on mount and automatically removes it with removeEventListener on unmount. Targets can be Ref, MaybeElement, a plain Element, Window, or Document. The listener is always called with the latest closure value — state changes never cause stale callbacks.

When no target is provided, the listener is attached to window.

import {
const useEventListener: {
<E extends keyof WindowEventMap>(event: Arrayable<E>, listener: Arrayable<(ev: WindowEventMap[E]) => void>, options?: MaybeObservable<boolean | AddEventListenerOptions>): () => void;
<E extends keyof WindowEventMap>(target: Window | Observable<OpaqueObject<Window> | null>, event: Arrayable<E>, listener: Arrayable<(ev: WindowEventMap[E]) => void>, options?: MaybeObservable<boolean | undefined | AddEventListenerOptions>): () => void;
<E extends keyof DocumentEventMap>(target: Document | Observable<OpaqueObject<Document> | null>, event: Arrayable<E>, listener: Arrayable<(ev: DocumentEventMap[E]) => void>, options?: MaybeObservable<boolean | AddEventListenerOptions>): () => void;
<E extends keyof HTMLElementEventMap>(target: MaybeEventTarget | MaybeEventTarget[] | null | undefined, event: Arrayable<E>, listener: Arrayable<(ev: HTMLElementEventMap[E]) => void>, options?: MaybeObservable<boolean | AddEventListenerOptions>): () => void;
<EventType = Event>(target: Observable<unknown> | ObservablePrimitive<unknown> | ReadonlyObservable<unknown> | null | undefined, event: Arrayable<string>, listener: Arrayable<GeneralEventListener<EventType>>, options?: MaybeObservable<boolean | AddEventListenerOptions>): () => void;
<EventType = Event>(target: MaybeEventTarget | EventTarget | null | undefined, event: Arrayable<string>, listener: Arrayable<GeneralEventListener<EventType>>, options?: MaybeObservable<boolean | AddEventListenerOptions>): () => void;
<EventType = Event>(target: EventTarget | null | undefined, event: Arrayable<string>, listener: Arrayable<GeneralEventListener<EventType>>, options?: MaybeObservable<boolean | AddEventListenerOptions>): () => void;
}
useEventListener
} from "@usels/web";
function
function Component(): null
Component
() {
useEventListener<"keydown">(event: Arrayable<"keydown">, listener: Arrayable<(ev: WindowEventMap["keydown"]) => void>, options?: MaybeObservable<boolean | AddEventListenerOptions>): () => void (+6 overloads)

Framework-agnostic event listener registration. Must be called inside a useScope factory — registration is deferred to onMount so that any other reactive setup in the same scope (e.g. useWhenever) gets to run first. Reactive Ref$ / Observable<EventTarget> targets are tracked via legendObserve and re-bound automatically when they change.

Overload 1: Omitted target — defaults to window.

useEventListener
("keydown", (
ev: WindowEventMap
ev
) => {
any
console
.
any
log
(
ev: WindowEventMap
ev
.
any
key
);
});
return null;
}
import { useRef$, useEventListener } from "@usels/web";
function Component() {
const el$ = useRef$<HTMLDivElement>();
useEventListener(el$, "click", (ev) => {
console.log("clicked", ev.target);
});
return <div ref={el$} />;
}

When an Ref$ or MaybeElement is passed as the target, the listener is automatically re-registered whenever the element changes.

const el$ = useRef$<HTMLButtonElement>();
useEventListener(el$, "pointerdown", (ev) => {
ev.preventDefault();
});
return <button ref={el$} />;
useEventListener(el$, ["mouseenter", "mouseleave"], (ev) => {
console.log(ev.type);
});
tsx useEventListener(el$, "click", [onClickA, onClickB]);
useEventListener(document, "visibilitychange", () => {
console.log(document.visibilityState);
});
useEventListener(el$, "scroll", onScroll, { passive: true });

The hook returns a cleanup function for imperative removal before unmount.

const stop = useEventListener("resize", onResize);
// remove the listener early
stop();

Plain element targets are not reactive. If you pass a plain HTMLElement or null value and that reference changes after mount (e.g. via useState), the hook does not detect the change. Use Ref$ or MaybeElement for targets that change over time.

// ❌ listener stays on the original element if el changes via state
const [el, setEl] = useState<HTMLDivElement | null>(null);
useEventListener(el, "click", handler);
// ✅ listener is re-registered automatically when el$ changes
const el$ = useRef$<HTMLDivElement>();
useEventListener(el$, "click", handler);
export interface GeneralEventListener<E = Event> {
(evt: E): void;
}
/**
* Single source of truth for the (target, event, listener, options)
* positional layout. Both `createEventListener` and the React wrapper
* call this so there is no duplicated arg-shape detection.
*/
export interface ParsedEventListenerArgs {
hasTarget: boolean;
/** index of the listener parameter inside the original args array */
listenerIdx: number;
target: any;
event: any;
listener: any;
options: any;
}
export declare function parseEventListenerArgs(args: any[]): ParsedEventListenerArgs;
/**
* Framework-agnostic event listener registration. Must be called inside a
* `useScope` factory — registration is deferred to `onMount` so that any
* other reactive setup in the same scope (e.g. `useWhenever`) gets to run
* first. Reactive `Ref$` / `Observable<EventTarget>` targets are tracked
* via `legendObserve` and re-bound automatically when they change.
*
* Overload 1: Omitted target — defaults to `window`.
*/
export declare function createEventListener<E extends keyof WindowEventMap>(event: Arrayable<E>, listener: Arrayable<(ev: WindowEventMap[E]) => void>, options?: MaybeObservable<boolean | AddEventListenerOptions>): () => void;
/**
* Overload 2: Explicit `Window` target.
*/
export declare function createEventListener<E extends keyof WindowEventMap>(target: Window | Observable<OpaqueObject<Window> | null>, event: Arrayable<E>, listener: Arrayable<(ev: WindowEventMap[E]) => void>, options?: MaybeObservable<boolean | undefined | AddEventListenerOptions>): () => void;
/**
* Overload 3: Explicit or reactive `Document` target.
*/
export declare function createEventListener<E extends keyof DocumentEventMap>(target: Document | Observable<OpaqueObject<Document> | null>, event: Arrayable<E>, listener: Arrayable<(ev: DocumentEventMap[E]) => void>, options?: MaybeObservable<boolean | AddEventListenerOptions>): () => void;
/**
* Overload 4: `MaybeEventTarget` target — supports Ref$, Observable<OpaqueObject<Element>>,
* Document, Window, or an array of those (Legend-State reactive).
*/
export declare function createEventListener<E extends keyof HTMLElementEventMap>(target: MaybeEventTarget | MaybeEventTarget[] | null | undefined, event: Arrayable<E>, listener: Arrayable<(ev: HTMLElementEventMap[E]) => void>, options?: MaybeObservable<boolean | AddEventListenerOptions>): () => void;
/**
* Overload 5: Observable<EventTarget> — reactive target (e.g.
* Observable<MediaQueryList>, Ref$<HTMLElement>, etc.).
*/
export declare function createEventListener<EventType = Event>(target: Observable<unknown> | ObservablePrimitive<unknown> | ReadonlyObservable<unknown> | null | undefined, event: Arrayable<string>, listener: Arrayable<GeneralEventListener<EventType>>, options?: MaybeObservable<boolean | AddEventListenerOptions>): () => void;
/**
* Overload 6: Combined `MaybeEventTarget | EventTarget` — handles union types where
* the target may be either a reactive MaybeEventTarget or a raw EventTarget.
*/
export declare function createEventListener<EventType = Event>(target: MaybeEventTarget | EventTarget | null | undefined, event: Arrayable<string>, listener: Arrayable<GeneralEventListener<EventType>>, options?: MaybeObservable<boolean | AddEventListenerOptions>): () => void;
/**
* Overload 7: Generic `EventTarget` fallback.
*/
export declare function createEventListener<EventType = Event>(target: EventTarget | null | undefined, event: Arrayable<string>, listener: Arrayable<GeneralEventListener<EventType>>, options?: MaybeObservable<boolean | AddEventListenerOptions>): () => void;

View on GitHub