-
-
Save gragland/ae701852ae6159c712d860a946cd7ca0 to your computer and use it in GitHub Desktop.
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 | |
); | |
}; |
@gragland i've just can't understand how to use 3rd parameter in useEventListener hook. May be you can one more usage sample?
@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])
}
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 });
@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!
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 () => {};
Might want to throw or warn when !isSupported
. Failing silently is a good way to cause extra debugging time.
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]
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.
@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);
@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.
@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.
You may add the following line element = !element ? window : element
before isSupported
constant in order to avoid the following error window is not defined
in Next.Js
projects. Also remove default element value in hook parameters!
I am wondering if the props like eventName
,event
and listener
will not change then why run effects again. If useEventListener
is run again why should we care about the new values, since the listener is already attached in the first place. What case am i missing?
@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?