Last active
June 1, 2025 11:18
-
-
Save nobu-sh/fb5a6266e9099aef71b4335801d4b8bf to your computer and use it in GitHub Desktop.
In a project I was drowning in event listeners and getting sick of repeating the same useEffect boilerplate. I found a sexier solution using Proxy.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const styles = { | |
| all: "unset", | |
| position: "absolute", | |
| top: "0px", | |
| left: "0px", | |
| width: "12rem", | |
| height: "3rem", | |
| backgroundColor: "rgba(0, 0, 0, 0.5)", | |
| borderRadius: "0.5rem", | |
| color: "white", | |
| zIndex: 9999, | |
| cursor: "pointer", | |
| display: "flex", | |
| alignItems: "center", | |
| justifyContent: "center", | |
| userSelect: "none", | |
| } as const; | |
| function MyComp() { | |
| const [count, setCount] = React.useState(0); | |
| const ref = React.useRef<HTMLButtonElement>(null); | |
| useOn["mousemove"](window, (event) => { | |
| if (!ref.current) return; | |
| const rect = ref.current.getBoundingClientRect(); | |
| const offsetX = event.pageX - rect.width / 2; | |
| const offsetY = event.pageY - rect.height / 2; | |
| ref.current.style.transform = `translate(${offsetX + count}px, ${offsetY}px)`; | |
| }, [count]); | |
| // We can hook refs as well! | |
| useOn["click"](ref, () => { | |
| ref.current!.style.backgroundColor = `hsl(${Math.random() * 360}, 100%, 30%)`; | |
| }); | |
| return ( | |
| <button | |
| ref={ref} | |
| onClick={() => setCount(c => c + 10)} | |
| style={styles} | |
| > | |
| {count === 0 ? "Click to offset" : `Offset Left: ${count}px`} | |
| </button> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import * as React from "react"; | |
| export type EventCallback<K extends keyof GlobalEventHandlersEventMap> = ( | |
| target: Document | Window | HTMLElement | React.Ref<HTMLElement>, | |
| cb: (ev: GlobalEventHandlersEventMap[K]) => void, | |
| deps?: React.DependencyList | |
| ) => void; | |
| export const useOn = new Proxy({}, { | |
| get(_, event: string) { | |
| return ( | |
| target: Document | Window | HTMLElement | React.RefObject<HTMLElement>, | |
| effect: EventListener, | |
| deps: React.DependencyList = [] | |
| ) => { | |
| // We will call callback artifically if deps change an a last event is available. | |
| const lastEventRef = React.useRef<Event | null>(null); | |
| React.useEffect(() => { | |
| function wrappedEffect(ev: Event) { | |
| lastEventRef.current = ev; | |
| effect(ev); | |
| } | |
| let emitter = "current" in target ? target.current : target; | |
| if (emitter) { | |
| emitter.addEventListener(event, wrappedEffect); | |
| return () => emitter?.removeEventListener(event, wrappedEffect); | |
| } | |
| // If ref is null, wait for it to become available | |
| if ("current" in target) { | |
| let frame: number; | |
| const check = () => { | |
| emitter = target.current; | |
| if (emitter) { | |
| emitter.addEventListener(event, wrappedEffect); | |
| } else { | |
| frame = requestAnimationFrame(check); | |
| } | |
| }; | |
| frame = requestAnimationFrame(check); | |
| return () => { | |
| if (frame) cancelAnimationFrame(frame); | |
| emitter?.removeEventListener(event, wrappedEffect); | |
| }; | |
| } | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, [event, ...deps]); | |
| // If deps change, call the last event if available | |
| React.useEffect(() => { | |
| if (lastEventRef.current) { | |
| effect(lastEventRef.current); | |
| } | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, deps); | |
| }; | |
| } | |
| }) as { [K in keyof GlobalEventHandlersEventMap]: EventCallback<K> }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment