Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
import { 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 = global){
// 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
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

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

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

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

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

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

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

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

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

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])
}
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.