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 | |
); | |
}; |
This comment has been minimized.
This comment has been minimized.
I was about to mention the same problem as @wikt0r |
This comment has been minimized.
This comment has been minimized.
Yeah modifying it like that would work I believe:
Putting a console log in the first useEffect only shows one update |
This comment has been minimized.
This comment has been minimized.
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]}) ? |
This comment has been minimized.
This comment has been minimized.
Element in hook parameter doesn't work. We can't get element in first render call. |
This comment has been minimized.
This comment has been minimized.
@wikt0r @msevestre Good point, just updated the post to utilize useCallback. |
This comment has been minimized.
This comment has been minimized.
@gragland i've just can't understand how to use 3rd parameter in useEventListener hook. May be you can one more usage sample? |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
@gragland I wrote a hook of my own like this for a case where I needed a /**
* @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])
} |
This comment has been minimized.
This comment has been minimized.
It's a simple mistake but you forgot to import useState in the example
|
This comment has been minimized.
This comment has been minimized.
@ikabirov The 3rd parameter is the element to add the event listener to. I've changed the default from |
This comment has been minimized.
This comment has been minimized.
Some ESLint configurations may complain about
To:
|
This comment has been minimized.
This comment has been minimized.
Might want to throw or warn when |
This comment has been minimized.
This comment has been minimized.
hello .that good to attach an event to HTML node |
This comment has been minimized.
This comment has been minimized.
there's a bug on line 38 in this code:
if the handler is changed then |
This comment has been minimized.
This comment has been minimized.
@yossisp it adds the real event listener just once on mount and removes it once on dismount, and the real event listener delegates to // Create event listener that calls handler function stored in ref
const eventListener = event => savedHandler.current(event); |
This comment has been minimized.
This comment has been minimized.
@jedwards1211 the problem is that event listeners are removed only when the component unmounts. But if the component rerenders the |
This comment has been minimized.
This comment has been minimized.
@yossisp I don't know what you observed in the debugger but you must be misunderstanding, because the semantics of |
This comment has been minimized.
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 linesavedHandler.current = handler;
It will run on every render.https://codesandbox.io/s/mqxy739vyy
Hooks are tricky. :)