Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
import { useState, useRef, useEffect, useCallback } from 'react';
// Usage
function App(){
// State for storing mouse coordinates
const [coords, setCoords] = useState({ x: 0, y: 0 });
// Event handler utilizing useCallback ...
// ... so that reference never changes.
const handler = useCallback(
({ clientX, clientY }) => {
// Update coordinates
setCoords({ x: clientX, y: clientY });
},
[setCoords]
);
// Add event listener using our hook
useEventListener('mousemove', handler);
return (
<h1>
The mouse position is ({coords.x}, {coords.y})
</h1>
);
}
// Hook
function useEventListener(eventName, handler, element = window){
// Create a ref that stores handler
const savedHandler = useRef();
// Update ref.current value if handler changes.
// This allows our effect below to always get latest handler ...
// ... without us needing to pass it in effect deps array ...
// ... and potentially cause effect to re-run every render.
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(
() => {
// Make sure element supports addEventListener
// On
const isSupported = element && element.addEventListener;
if (!isSupported) return;
// Create event listener that calls handler function stored in ref
const eventListener = event => savedHandler.current(event);
// Add event listener
element.addEventListener(eventName, eventListener);
// Remove event listener on cleanup
return () => {
element.removeEventListener(eventName, eventListener);
};
},
[eventName, element] // Re-run if eventName or element changes
);
};
@wikt0r

This comment has been minimized.

Copy link

wikt0r commented Mar 28, 2019

I think useRef in useEventListener is useless, because you always pass new anonymous function to the hook in every render from App. Try insert a console.log before the line savedHandler.current = handler; It will run on every render.

https://codesandbox.io/s/mqxy739vyy

Hooks are tricky. :)

@msevestre

This comment has been minimized.

Copy link

msevestre commented Mar 28, 2019

I was about to mention the same problem as @wikt0r
Using a useCallback in the caller would probably do the trick? It kinds of leaks the abstraction a bit however

@msevestre

This comment has been minimized.

Copy link

msevestre commented Mar 28, 2019

Yeah modifying it like that would work I believe:

// Usage
function App(){
  // State for storing mouse coordinates
  const [coords, setCoords] = useState([0, 0]);
  
const eventListener = useCallback(
    ({ clientX, clientY }) => {
      setCoords([clientX, clientY]);
    },
    [setCoords]
  );

  // Add event listener using our hook
  useEventListener("mousemove", eventListener);  
  return (
    <h1>
      The mouse position is ({coords.x}, {coords.y})
    </h1>
  );
}
...

Putting a console log in the first useEffect only shows one update

@oygen87

This comment has been minimized.

Copy link

oygen87 commented Apr 15, 2019

we display the coords as ({coords.x}, {coords.y}) .

but where do we define x and y keys on coords?

shouldnt we display them as ({coords[0]}, {coords[1]}) ?

@ikabirov

This comment has been minimized.

Copy link

ikabirov commented Apr 19, 2019

Element in hook parameter doesn't work. We can't get element in first render call.

@gragland

This comment has been minimized.

Copy link
Owner Author

gragland commented May 2, 2019

@wikt0r @msevestre Good point, just updated the post to utilize useCallback.
@oygen87 Woops, this was a typo. Fixed!
@ikabirov Sorry could you explain what you mean?

@ikabirov

This comment has been minimized.

Copy link

ikabirov commented May 2, 2019

@gragland i've just can't understand how to use 3rd parameter in useEventListener hook. May be you can one more usage sample?

@DamianEl

This comment has been minimized.

Copy link

DamianEl commented May 8, 2019

@ikabirov I think I came across the same issue, this could help you.

@jedwards1211

This comment has been minimized.

Copy link

jedwards1211 commented Jun 7, 2019

@gragland I wrote a hook of my own like this for a case where I needed a passive: false wheel listener to prevent scroll events. You ought to make this hook allow a third options parameter for capture, passive, etc.

/**
 * @flow
 * @prettier
 */

import * as React from 'react'

export default function useEventListener(ref: $ReadOnly<{current: any}>, event: string, listener: (e: any) => any, options?: EventListenerOptionsOrUseCapture): void {
  const capture = options instanceof Object ? options.capture : options === true
  const once = options instanceof Object ? options.once : false
  const passive = options instanceof Object ? options.passive : false
  React.useEffect(() => {
    if (!ref.current) return
    ref.current.addEventListener(event, listener, options)
    return () => {
      ref.current.removeEventListener(event, listener, options)
    }
  }, [ref.current, event, listener, capture, once, passive])
}
@Dygerydoo

This comment has been minimized.

Copy link

Dygerydoo commented Jul 1, 2019

It's a simple mistake but you forgot to import useState in the example

import { useRef, useEffect, useCallback } from 'react';

// Usage
function App(){
  // State for storing mouse coordinates
  const [coords, setCoords] = useState({ x: 0, y: 0 });
@gragland

This comment has been minimized.

Copy link
Owner Author

gragland commented Jul 2, 2019

@ikabirov The 3rd parameter is the element to add the event listener to. I've changed the default from global to window to make it a little more clear to people not used to seeing global (which is defined if node is rendering server-side, but irrelevant in this case since useEffect is only run client-side).
@Dygerydoo Thanks, fixed!

@josebrito

This comment has been minimized.

Copy link

josebrito commented Aug 12, 2019

Some ESLint configurations may complain about eslint/consistent-return with this approach. I suggest returning a no-op function instead. Change:

if (!isSupported) return;

To:

if (!isSupported) return () => {};
@statianzo

This comment has been minimized.

Copy link

statianzo commented Aug 26, 2019

Might want to throw or warn when !isSupported. Failing silently is a good way to cause extra debugging time.

@setbap

This comment has been minimized.

Copy link

setbap commented Sep 14, 2019

hello .that good to attach an event to HTML node
so I add a state to reference HTML node ( i can't speak English well :) )
the code that I forked from code sandbox:https://codesandbox.io/s/useeventlistener-yyxnq
ref, setRef is optional, if we don't use it evet attached to the window
[i think that not be in a nice way but I learn to react and I want to suggest my way to help me learn new thing]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.