Skip to content

Instantly share code, notes, and snippets.

@nobu-sh
Last active June 1, 2025 11:18
Show Gist options
  • Select an option

  • Save nobu-sh/fb5a6266e9099aef71b4335801d4b8bf to your computer and use it in GitHub Desktop.

Select an option

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