Skip to content

use-event-listener

use-event-listener is a declarative React hook that simplifies DOM event handling with automatic lifecycle management.

It eliminates repetitive add/removeEventListener boilerplate while preventing memory leaks through intelligent cleanup on unmount or dependency changes.

This hook supports flexible targeting via either a target prop or setElementRef callback, and features built-in conditional binding. It fully supports all standard AddEventListenerOptions including capture, once, passive, and signal.

Features

  • Declarative event handling: Attach DOM events via a clean, hook-based API
  • Flexible targeting: Bind listeners using a target function or setElementRef callback
  • Automatic cleanup: Listeners are removed on unmount or dependency changes
  • Reactive rebinding: Reattaches automatically when target, event, or options change
  • Conditional binding: Toggle listener attachment with shouldInjectEvent
  • Full options support: Supports all AddEventListenerOptions (capture, once, passive, signal)
  • Stable references: Avoids unnecessary add/remove cycles across re-renders
  • Ref-free usage: No manual refs needed when using setElementRef

Usage Note

  • Do not pass target prop if using setElementRef and vise-versa.

Problem It Solves

Boilerplate Reduction

Manually managing event listeners in React components leads to verbose, repetitive and error-prone code with potential memory leaks. See the below implementation:

tsx
// ❌ Problematic approach which is redundant and verbose
function Component() {
   const [scrollY, setScrollY] = useState(0)

   useEffect(() => {
      const handleScroll = () => {
         setScrollY(window.scrollY)
      }

      window.addEventListener('scroll', handleScroll)
      return () => window.removeEventListener('scroll', handleScroll) // Doing proper cleanup on unmount
   }, [])

   return <div>Current: {scrollY}</div>
}

How use-event-listener solves it:

  • Eliminates repetitive addEventListener/removeEventListener code
  • Reduces component complexity by abstracting event handling logic
  • Makes conditional event attachment predictable and declarative
  • Simplifies dynamic event binding when below things happens:-
    • Component unmounts
    • target element changes
    • event type changes
    • Any of options params:- (shouldInjectEvent, capture, once, passive, signal) gets changed
tsx
// ✅ Clean, declarative approach
function Component() {
   const [scrollY, setScrollY] = useState(0)
   const breakpoint = useEventListener({
      target: () => window,
      event: 'scroll',
      handler: () => {
         setScrollY(window.scrollY)
      },
   })

   return <div>Current: {scrollY}</div>
}
Performance Benefits
  • Stable references accross re-renders which prevents listeners from being repeatedly added/removed
  • Efficient dependency tracking
  • Prevents memory leaks caused by missed or incorrect cleanup
  • Eliminates manual lifecycle management for DOM event listeners
  • Avoids unnecessary listener re-creation across component re-renders

Parameters

ParameterTypeRequiredDefault ValueDescription
targetEvTarget-Target element on which the event is listened to.
eventstring-Event name (e.g. 'click', 'keydown')
handlerEvHandlerundefinedEvent listener callback function
optionsEvOptionsundefinedStandard Options and Feature flags

Options Parameter

The options parameter supports all standard AddEventListenerOptions and introduces extra custom properties to control conditional event binding.

Standard AddEventListenerOptions

PropertyTypeDefaultDescription
capturebooleanfalseIf true, the listener will be triggered during the capture phase
oncebooleanfalseIf true, the listener will be automatically removed after being triggered once
passivebooleanfalseIf true, indicates that the function will never call preventDefault()
signalAbortSignalundefinedAn AbortSignal that can be used to remove the event listener

Custom Options

PropertyTypeDefaultDescription
shouldInjectEventboolean | anytrueControls whether the event listener should be attached. When false, the event listener is not added

Type Definitions

Details
ts
export type EvTarget = () => EventTarget | null
export type EvHandler = (event: Event) => void

export interface EvOptions extends AddEventListenerOptions {
   // Standard AddEventListenerOptions:
   capture?: boolean
   once?: boolean
   passive?: boolean
   signal?: AbortSignal

   // Custom option:
   shouldInjectEvent?: boolean | any // Controls whether the event should be attached
}

Return Value(s)

This hook returns an object that includes a setter function, allowing you to observe and manage the target element through its ref attribute.

PropertyTypeDescription
setElementRefFunctionA ref callback that observes the target element for event listening

Return Types

Details
ts
export type UseEventListenerReturnValues = {
   setElementRef: (elementNode: HTMLElement | null) => void
}

Common Use Cases

  • Adding dom events (e.g 'click', 'keydown', 'resize', 'scroll')

Usage Examples

Usage with setElementRef(no manual creation of ref)

ts
import { useState } from 'react'
import { useEventListener } from 'classic-react-hooks'

export default function ConditionalExample() {
   const [counter, setCounter] = useState(0)
   const { setElementRef } = useEventListener({
      event: 'click',
      handler: () => {
         console.log(counter)
      },
   })

   return (
      <div>
         <button onClick={() => setCounter((c) => c + 1)}>update counter {counter}</button>
         <div ref={setElementRef}>log value</div>
      </div>
   )
}

Basic Click Handler

Exampls
ts
import { useRef } from 'react'
import { useEventListener } from 'classic-react-hooks'

export default function ClickExample() {
   const buttonRef = useRef<HTMLButtonElement>(null)

   useEventListener({
      target: () => buttonRef.current,
      event: 'click',
      handler: (e) => {
         console.log('Button clicked!', e)
      },
   })

   return <button ref={buttonRef}>Click me</button>
}

Listening Window Event

Example
ts
import { useEventListener } from 'classic-react-hooks'

export default function WindowExample() {
   useEventListener({
      target: () => window,
      event: 'resize',
      handler: (e) => {
         console.log('Window resized:', window.innerWidth, window.innerHeight)
      },
   })

   return <div>Resize the window and check console</div>
}

Conditional Event Listening

Example
ts
import { useState } from 'react'
import { useEventListener } from 'classic-react-hooks'

export default function ConditionalExample() {
   const [isListening, setIsListening] = useState(true) 

   useEventListener({
      target: () => document,
      event: 'keydown',
      handler: (e) => {
         console.log('Key pressed:', e.key)
      },
      options: {
         shouldInjectEvent: isListening, // Only listen when enabled
      },
   })

   return (
      <div>
         <button onClick={() => setIsListening(!isListening)}>{isListening ? 'Stop' : 'Start'} Listening</button>
         <p>Press any key (when listening is enabled)</p>
      </div>
   )
}