Skip to main content

useEventListener

Listen to an event on any target, such as window or document, for as long as the component is mounted.

Source
Loading...
function App() {
  const [listening, setListening] = useState(true)
  const [key, setKey] = useState('-')

  // A function, so that `window` is read in the browser and not while the
  // page renders on the server. `null` listens on nothing.
  useEventListener(
    () => (listening ? window : null),
    'keydown',
    (event) => setKey(event.key === ' ' ? 'Space' : event.key),
  )

  return (
    <div style={{ userSelect: 'none' }}>
      <div
        style={{
          padding: '2rem',
          marginBottom: '0.5rem',
          border: '1px solid currentColor',
          borderRadius: 8,
          textAlign: 'center',
        }}
      >
        {listening ? 'Press any key' : 'Not listening'}
      </div>
      <div>last key: {key}</div>
      <label>
        <input
          type="checkbox"
          checked={listening}
          onChange={(event) => setListening(event.target.checked)}
        />{' '}
        Listen on window
      </label>
    </div>
  )
}

Import​

import { useEventListener } from '@tremolo-ui/react'

Usage​

useEventListener(target, 'keydown', handler, options)

The event name types the handler: 'keydown' gives it a KeyboardEvent.

All of the arguments can be written inline. The handler is read on every event, and the target and the options are compared by what they resolve to, not by identity. The listener is re-attached only when the element, the event name or one of the options actually changes.

The target​

The target is an EventTarget, null, or a function returning either.

  • null listens on nothing. Pass it to stop listening for a while
  • A function is called after every render, in the browser. Use one to reach an element through a ref, and for window and document on a page that is also rendered on the server, where neither exists while rendering
const ref = useRef(null)
useEventListener(() => ref.current, 'pointerenter', handler)
useEventListener(() => window, 'resize', handler)

There is no default target: pass document or window by name.

Options​

The fourth argument is the options of addEventListener: a boolean for capture, or an object with capture, once and passive. signal is left out, since the hook already decides when the listener goes.

useEventListener(() => window, 'touchmove', handler, { passive: false })

Svelte and Vue​

This hook is React only. Both frameworks already have a way to do the same, so @tremolo-ui/svelte and @tremolo-ui/vue do not repeat it.

<svelte:window> and <svelte:document> listen for as long as the component is mounted:

<svelte:window onkeydown={(event) => (key = event.key)} />

For any other target, or to listen only while something holds, return on() from svelte/events out of an $effect. It gives back the function that removes the listener, which is what the effect's cleanup wants:

import { on } from 'svelte/events'

$effect(() => {
if (!listening) return
return on(window, 'keydown', (event) => (key = event.key))
})

Removing the listener early​

The return value removes the listener. It stays removed until the target, the event name or the options change, so a rerender does not bring it back:

const stop = useEventListener(() => window, 'pointermove', (event) => {
if (done(event)) stop()
})

For something to switch on and off, prefer a null target: it says in the render whether the listener is there.