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 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 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 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 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 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 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 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 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 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 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 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 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 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 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]

@yossisp

This comment has been minimized.

Copy link

@yossisp yossisp commented Jan 16, 2020

there's a bug on line 38 in this code:

useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);

if the handler is changed then removeEventListener must also be called.

@jedwards1211

This comment has been minimized.

Copy link

@jedwards1211 jedwards1211 commented Jan 16, 2020

@yossisp it adds the real event listener just once on mount and removes it once on dismount, and the real event listener delegates to handler:

      // Create event listener that calls handler function stored in ref
      const eventListener = event => savedHandler.current(event);
@yossisp

This comment has been minimized.

Copy link

@yossisp yossisp commented Jan 20, 2020

@jedwards1211 the problem is that event listeners are removed only when the component unmounts. But if the component rerenders the element will change which will add an event listener without removing the old one. This is what happened to me.

@jedwards1211

This comment has been minimized.

Copy link

@jedwards1211 jedwards1211 commented Jan 20, 2020

@yossisp I don't know what you observed in the debugger but you must be misunderstanding, because the semantics of useEffect guarantees that the cleanup function will run (removing the old listener) before the function runs again (adding the new listener). And the cleanup function's closure will have element bound to the old element.

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.