Use useCallback.

This commit is contained in:
Jay
2025-07-23 17:56:39 -04:00
parent 051c6602b5
commit 2737c6bf9e
3 changed files with 197 additions and 246 deletions
+78 -104
View File
@@ -1,4 +1,4 @@
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef, useCallback } from "react";
import type { Dispatch, SetStateAction } from "react"; import type { Dispatch, SetStateAction } from "react";
import type { CartesianSpace } from "../types"; import type { CartesianSpace } from "../types";
import { minmax } from "../util"; import { minmax } from "../util";
@@ -12,6 +12,22 @@ function isTouchEvent(event: Event): event is TouchEvent {
return "touches" in event; return "touches" in event;
} }
function extractEventCoordinates(event: MouseEvent | TouchEvent): {
clientX: number;
clientY: number;
} {
if (isTouchEvent(event)) {
return {
clientX: event.touches[0].clientX,
clientY: event.touches[0].clientY,
};
}
return {
clientX: event.clientX,
clientY: event.clientY,
};
}
export function useCrosshair({ export function useCrosshair({
origin, origin,
dimensions, dimensions,
@@ -26,19 +42,6 @@ export function useCrosshair({
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const crosshairRef = useRef<HTMLDivElement>(null); const crosshairRef = useRef<HTMLDivElement>(null);
// Construct event handler refs
// Prevents unnecessary function recreation
const calculatePositionsRef = useRef((_: MouseEvent | TouchEvent) => {});
const startCrosshairInteractionRef = useRef(
(_: MouseEvent | TouchEvent) => {},
);
const processCrosshairInteractionRef = useRef(
(_: MouseEvent | TouchEvent) => {},
);
const endCrosshairInteractionRef = useRef((_: MouseEvent | TouchEvent) => {});
// Store dependencies as refs
// Always use latest values
const originRef = useRef(origin); const originRef = useRef(origin);
const dimensionsRef = useRef(dimensions); const dimensionsRef = useRef(dimensions);
@@ -47,121 +50,92 @@ export function useCrosshair({
dimensionsRef.current = dimensions; dimensionsRef.current = dimensions;
}, [origin, dimensions]); }, [origin, dimensions]);
// Update handler functions when dependencies change via reference const calculatePositions = useCallback(
(event: MouseEvent | TouchEvent) => {
useEffect(() => {
calculatePositionsRef.current = (event: MouseEvent | TouchEvent) => {
const orig = originRef.current; const orig = originRef.current;
const dims = dimensionsRef.current; const dims = dimensionsRef.current;
const clientX = isTouchEvent(event) const { clientX, clientY } = extractEventCoordinates(event);
? event.touches[0].clientX
: event.clientX;
const clientY = isTouchEvent(event)
? event.touches[0].clientY
: event.clientY;
const xPos = minmax(clientX - orig.x, 0, dims.x - 1); const xPos = minmax(clientX - orig.x, 0, dims.x - 1);
const yPos = minmax(clientY - orig.y, 0, dims.y - 1); const yPos = minmax(clientY - orig.y, 0, dims.y - 1);
setXPosition(xPos); setXPosition(xPos);
setYPosition(yPos); setYPosition(yPos);
}; },
[setXPosition, setYPosition],
);
startCrosshairInteractionRef.current = (event: MouseEvent | TouchEvent) => { const processCrosshairInteraction = useCallback(
(event: MouseEvent | TouchEvent) => {
event.preventDefault(); event.preventDefault();
calculatePositionsRef.current(event); calculatePositions(event);
},
[calculatePositions],
);
const endCrosshairInteraction = useCallback(
(event: MouseEvent | TouchEvent) => {
setIsDragging(false);
if (!isTouchEvent(event)) {
document.removeEventListener("mousemove", processCrosshairInteraction);
document.removeEventListener("mouseup", endCrosshairInteraction);
} else {
document.removeEventListener("touchmove", processCrosshairInteraction);
document.removeEventListener("touchend", endCrosshairInteraction);
document.removeEventListener("touchcancel", endCrosshairInteraction);
}
},
[processCrosshairInteraction],
);
const startCrosshairInteraction = useCallback(
(event: MouseEvent | TouchEvent) => {
event.preventDefault();
calculatePositions(event);
setIsDragging(true); setIsDragging(true);
if (!isTouchEvent(event)) { if (!isTouchEvent(event)) {
document.addEventListener( document.addEventListener("mousemove", processCrosshairInteraction);
"mousemove", document.addEventListener("mouseup", endCrosshairInteraction, {
processCrosshairInteractionRef.current, passive: true,
); });
document.addEventListener(
"mouseup",
endCrosshairInteractionRef.current,
{ passive: true },
);
} else { } else {
document.addEventListener( document.addEventListener("touchmove", processCrosshairInteraction);
"touchmove", document.addEventListener("touchend", endCrosshairInteraction, {
processCrosshairInteractionRef.current, passive: true,
); });
document.addEventListener( document.addEventListener("touchcancel", endCrosshairInteraction, {
"touchend", passive: true,
endCrosshairInteractionRef.current, });
{ passive: true },
);
document.addEventListener(
"touchcancel",
endCrosshairInteractionRef.current,
{ passive: true },
);
} }
}; },
[calculatePositions, processCrosshairInteraction, endCrosshairInteraction],
);
processCrosshairInteractionRef.current = (
event: MouseEvent | TouchEvent,
) => {
event.preventDefault();
calculatePositionsRef.current(event);
};
endCrosshairInteractionRef.current = (event: MouseEvent | TouchEvent) => {
setIsDragging(false);
if (!isTouchEvent(event)) {
document.removeEventListener(
"mousemove",
processCrosshairInteractionRef.current,
);
document.removeEventListener(
"mouseup",
endCrosshairInteractionRef.current,
);
} else {
document.removeEventListener(
"touchmove",
processCrosshairInteractionRef.current,
);
document.removeEventListener(
"touchend",
endCrosshairInteractionRef.current,
);
document.removeEventListener(
"touchcancel",
endCrosshairInteractionRef.current,
);
}
};
}, []);
// Set up entry listeners
useEffect(() => { useEffect(() => {
const currentRef = crosshairRef.current; const currentRef = crosshairRef.current;
if (currentRef) { if (currentRef) {
currentRef.addEventListener( currentRef.addEventListener("mousedown", startCrosshairInteraction);
"mousedown", currentRef.addEventListener("touchstart", startCrosshairInteraction);
startCrosshairInteractionRef.current,
);
currentRef.addEventListener(
"touchstart",
startCrosshairInteractionRef.current,
);
} }
return () => { return () => {
if (currentRef) { if (currentRef) {
currentRef.removeEventListener( currentRef.removeEventListener("mousedown", startCrosshairInteraction);
"mousedown", currentRef.removeEventListener("touchstart", startCrosshairInteraction);
startCrosshairInteractionRef.current,
);
currentRef.removeEventListener(
"touchstart",
startCrosshairInteractionRef.current,
);
} }
document.removeEventListener("mousemove", processCrosshairInteraction);
document.removeEventListener("mouseup", endCrosshairInteraction);
document.removeEventListener("touchmove", processCrosshairInteraction);
document.removeEventListener("touchend", endCrosshairInteraction);
document.removeEventListener("touchcancel", endCrosshairInteraction);
}; };
}, []); }, [
startCrosshairInteraction,
processCrosshairInteraction,
endCrosshairInteraction,
]);
return { crosshairRef, isDragging }; return { crosshairRef, isDragging };
} }
+19 -23
View File
@@ -1,4 +1,4 @@
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect, useCallback } from "react";
import type { RefObject } from "react"; import type { RefObject } from "react";
export function handleScroll( export function handleScroll(
@@ -6,7 +6,7 @@ export function handleScroll(
scrollDelta: number, scrollDelta: number,
handleIncrement: () => void, handleIncrement: () => void,
handleDecrement: () => void, handleDecrement: () => void,
) { ): number {
const newLength = prevLength + scrollDelta; const newLength = prevLength + scrollDelta;
if (Math.abs(newLength) > 50) { if (Math.abs(newLength) > 50) {
@@ -47,42 +47,38 @@ export function useScroll<T extends HTMLElement>({
deltaYMultiplierRef.current = deltaYMultiplier; deltaYMultiplierRef.current = deltaYMultiplier;
}, [onScrollUp, onScrollDown, deltaYMultiplier]); }, [onScrollUp, onScrollDown, deltaYMultiplier]);
const handleWheelEventRef = useRef((_: WheelEvent) => {}); const handleWheelEvent = useCallback((event: WheelEvent) => {
event.preventDefault();
useEffect(() => { setScrollLength((prev) =>
handleWheelEventRef.current = (event: WheelEvent) => { handleScroll(
event.preventDefault(); prev,
event.deltaY * deltaYMultiplierRef.current,
setScrollLength((prev) => onScrollDownRef.current,
handleScroll( onScrollUpRef.current,
prev, ),
event.deltaY * deltaYMultiplierRef.current, );
onScrollDownRef.current,
onScrollUpRef.current,
),
);
};
}, []); }, []);
function addScrollListener() { const addScrollListener = useCallback(() => {
const currentRef = targetRef.current; const currentRef = targetRef.current;
if (currentRef) { if (currentRef) {
currentRef.addEventListener("wheel", handleWheelEventRef.current); currentRef.addEventListener("wheel", handleWheelEvent);
} }
} }, [handleWheelEvent, targetRef]);
function removeScrollListener() { const removeScrollListener = useCallback(() => {
const currentRef = targetRef.current; const currentRef = targetRef.current;
if (currentRef) { if (currentRef) {
currentRef.removeEventListener("wheel", handleWheelEventRef.current); currentRef.removeEventListener("wheel", handleWheelEvent);
} }
} }, [handleWheelEvent, targetRef]);
useEffect(() => { useEffect(() => {
return () => { return () => {
removeScrollListener(); removeScrollListener();
}; };
}, []); }, [removeScrollListener]);
return { return {
addScrollListener, addScrollListener,
+100 -119
View File
@@ -1,5 +1,5 @@
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect, useCallback } from "react";
import type { Dispatch, SetStateAction, RefObject } from "react"; import type { Dispatch, SetStateAction } from "react";
import { minmax } from "../util"; import { minmax } from "../util";
import type { CartesianSpace } from "../types"; import type { CartesianSpace } from "../types";
import { useScroll } from "./scroll"; import { useScroll } from "./scroll";
@@ -26,6 +26,20 @@ function chooseValueByDirection(
return direction === Direction.HORIZONTAL ? xValue : yValue; return direction === Direction.HORIZONTAL ? xValue : yValue;
} }
function extractEventCoordinate(
event: MouseEvent | TouchEvent,
direction: Direction,
): number {
if (isTouchEvent(event)) {
return chooseValueByDirection(
direction,
event.touches[0].clientX,
event.touches[0].clientY,
);
}
return chooseValueByDirection(direction, event.clientX, event.clientY);
}
export function useSlider({ export function useSlider({
direction, direction,
origin, origin,
@@ -36,21 +50,10 @@ export function useSlider({
origin: CartesianSpace; origin: CartesianSpace;
dimensions: CartesianSpace; dimensions: CartesianSpace;
setPosition: Dispatch<SetStateAction<number>>; setPosition: Dispatch<SetStateAction<number>>;
}): { sliderRef: RefObject<HTMLDivElement | null>; isDragging: boolean } { }) {
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const sliderRef = useRef<HTMLDivElement>(null); const sliderRef = useRef<HTMLDivElement>(null);
// Construct event handler refs
// Prevents unnecessary function recreation
const calculatePositionRef = useRef((_: MouseEvent | TouchEvent) => {});
const startSliderInteractionRef = useRef((_: MouseEvent | TouchEvent) => {});
const processSliderInteractionRef = useRef(
(_: MouseEvent | TouchEvent) => {},
);
const endSliderInteractionRef = useRef((_: MouseEvent | TouchEvent) => {});
// Store dependencies as refs
// Always use latest values
const directionRef = useRef(direction); const directionRef = useRef(direction);
const originRef = useRef(origin); const originRef = useRef(origin);
const dimensionsRef = useRef(dimensions); const dimensionsRef = useRef(dimensions);
@@ -61,147 +64,125 @@ export function useSlider({
dimensionsRef.current = dimensions; dimensionsRef.current = dimensions;
}, [direction, origin, dimensions]); }, [direction, origin, dimensions]);
// Setup scroll handlers // Setup drag handlers
const handleScrollUp = () => { const calculatePosition = useCallback(
const dir = directionRef.current; (event: MouseEvent | TouchEvent) => {
const dims = dimensionsRef.current;
setPosition((prev: number) =>
minmax(prev - 1, 0, chooseValueByDirection(dir, dims.x, dims.y)),
);
};
const handleScrollDown = () => {
const dir = directionRef.current;
const dims = dimensionsRef.current;
setPosition((prev: number) =>
minmax(prev + 1, 0, chooseValueByDirection(dir, dims.x, dims.y)),
);
};
// Initialize scroll handling
const { addScrollListener, removeScrollListener } = useScroll({
targetRef: sliderRef,
onScrollUp: handleScrollUp,
onScrollDown: handleScrollDown,
});
// Update handler functions when dependencies change via reference
useEffect(() => {
calculatePositionRef.current = (event: MouseEvent | TouchEvent) => {
const dir = directionRef.current; const dir = directionRef.current;
const orig = originRef.current; const orig = originRef.current;
const dims = dimensionsRef.current; const dims = dimensionsRef.current;
const clientCoord = isTouchEvent(event) const clientCoord = extractEventCoordinate(event, dir);
? chooseValueByDirection(
dir,
event.touches[0].clientX,
event.touches[0].clientY,
)
: chooseValueByDirection(dir, event.clientX, event.clientY);
const positionValue = minmax( const positionValue = minmax(
clientCoord - chooseValueByDirection(dir, orig.x, orig.y), clientCoord - chooseValueByDirection(dir, orig.x, orig.y),
0, 0,
chooseValueByDirection(dir, dims.x, dims.y), chooseValueByDirection(dir, dims.x, dims.y),
); );
setPosition(positionValue); setPosition(positionValue);
}; },
[setPosition],
);
startSliderInteractionRef.current = (event: MouseEvent | TouchEvent) => { const processSliderInteraction = useCallback(
(event: MouseEvent | TouchEvent) => {
event.preventDefault(); event.preventDefault();
calculatePositionRef.current(event); calculatePosition(event);
},
[calculatePosition],
);
const endSliderInteraction = useCallback(
(event: MouseEvent | TouchEvent) => {
setIsDragging(false);
if (!isTouchEvent(event)) {
document.removeEventListener("mousemove", processSliderInteraction);
document.removeEventListener("mouseup", endSliderInteraction);
} else {
document.removeEventListener("touchmove", processSliderInteraction);
document.removeEventListener("touchend", endSliderInteraction);
document.removeEventListener("touchcancel", endSliderInteraction);
}
},
[processSliderInteraction],
);
const startSliderInteraction = useCallback(
(event: MouseEvent | TouchEvent) => {
event.preventDefault();
calculatePosition(event);
setIsDragging(true); setIsDragging(true);
if (!isTouchEvent(event)) { if (!isTouchEvent(event)) {
document.addEventListener( document.addEventListener("mousemove", processSliderInteraction);
"mousemove", document.addEventListener("mouseup", endSliderInteraction, {
processSliderInteractionRef.current,
);
document.addEventListener("mouseup", endSliderInteractionRef.current, {
passive: true, passive: true,
}); });
} else { } else {
document.addEventListener( document.addEventListener("touchmove", processSliderInteraction);
"touchmove", document.addEventListener("touchend", endSliderInteraction, {
processSliderInteractionRef.current, passive: true,
); });
document.addEventListener("touchend", endSliderInteractionRef.current, { document.addEventListener("touchcancel", endSliderInteraction, {
passive: true, passive: true,
}); });
document.addEventListener(
"touchcancel",
endSliderInteractionRef.current,
{
passive: true,
},
);
} }
}; },
[calculatePosition, processSliderInteraction, endSliderInteraction],
);
processSliderInteractionRef.current = (event: MouseEvent | TouchEvent) => { // Setup scroll handlers
event.preventDefault(); const handleScrollUp = useCallback(() => {
calculatePositionRef.current(event); const dir = directionRef.current;
}; const dims = dimensionsRef.current;
endSliderInteractionRef.current = (event: MouseEvent | TouchEvent) => { setPosition((prev: number) =>
setIsDragging(false); minmax(prev - 1, 0, chooseValueByDirection(dir, dims.x, dims.y)),
if (!isTouchEvent(event)) { );
document.removeEventListener( }, [setPosition]);
"mousemove",
processSliderInteractionRef.current, const handleScrollDown = useCallback(() => {
); const dir = directionRef.current;
document.removeEventListener( const dims = dimensionsRef.current;
"mouseup",
endSliderInteractionRef.current, setPosition((prev: number) =>
); minmax(prev + 1, 0, chooseValueByDirection(dir, dims.x, dims.y)),
} else { );
document.removeEventListener( }, [setPosition]);
"touchmove",
processSliderInteractionRef.current, const { addScrollListener, removeScrollListener } = useScroll({
); targetRef: sliderRef,
document.removeEventListener( onScrollUp: handleScrollUp,
"touchend", onScrollDown: handleScrollDown,
endSliderInteractionRef.current, });
);
document.removeEventListener(
"touchcancel",
endSliderInteractionRef.current,
);
}
};
}, []);
// Set up entry listeners // Set up entry listeners
useEffect(() => { useEffect(() => {
const currentRef = sliderRef.current; const currentRef = sliderRef.current;
if (currentRef) { if (currentRef) {
addScrollListener(); addScrollListener();
currentRef.addEventListener( currentRef.addEventListener("mousedown", startSliderInteraction);
"mousedown", currentRef.addEventListener("touchstart", startSliderInteraction);
startSliderInteractionRef.current,
);
currentRef.addEventListener(
"touchstart",
startSliderInteractionRef.current,
);
} }
return () => { return () => {
if (currentRef) { if (currentRef) {
removeScrollListener(); removeScrollListener();
currentRef.removeEventListener( currentRef.removeEventListener("mousedown", startSliderInteraction);
"mousedown", currentRef.removeEventListener("touchstart", startSliderInteraction);
startSliderInteractionRef.current,
);
currentRef.removeEventListener(
"touchstart",
startSliderInteractionRef.current,
);
} }
document.removeEventListener("mousemove", processSliderInteraction);
document.removeEventListener("mouseup", endSliderInteraction);
document.removeEventListener("touchmove", processSliderInteraction);
document.removeEventListener("touchend", endSliderInteraction);
document.removeEventListener("touchcancel", endSliderInteraction);
}; };
}, []); }, [
addScrollListener,
removeScrollListener,
startSliderInteraction,
processSliderInteraction,
endSliderInteraction,
]);
return { sliderRef, isDragging }; return { sliderRef, isDragging };
} }