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
+100 -119
View File
@@ -1,5 +1,5 @@
import { useState, useRef, useEffect } from "react";
import type { Dispatch, SetStateAction, RefObject } from "react";
import { useState, useRef, useEffect, useCallback } from "react";
import type { Dispatch, SetStateAction } from "react";
import { minmax } from "../util";
import type { CartesianSpace } from "../types";
import { useScroll } from "./scroll";
@@ -26,6 +26,20 @@ function chooseValueByDirection(
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({
direction,
origin,
@@ -36,21 +50,10 @@ export function useSlider({
origin: CartesianSpace;
dimensions: CartesianSpace;
setPosition: Dispatch<SetStateAction<number>>;
}): { sliderRef: RefObject<HTMLDivElement | null>; isDragging: boolean } {
}) {
const [isDragging, setIsDragging] = useState(false);
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 originRef = useRef(origin);
const dimensionsRef = useRef(dimensions);
@@ -61,147 +64,125 @@ export function useSlider({
dimensionsRef.current = dimensions;
}, [direction, origin, dimensions]);
// Setup scroll handlers
const handleScrollUp = () => {
const dir = directionRef.current;
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) => {
// Setup drag handlers
const calculatePosition = useCallback(
(event: MouseEvent | TouchEvent) => {
const dir = directionRef.current;
const orig = originRef.current;
const dims = dimensionsRef.current;
const clientCoord = isTouchEvent(event)
? chooseValueByDirection(
dir,
event.touches[0].clientX,
event.touches[0].clientY,
)
: chooseValueByDirection(dir, event.clientX, event.clientY);
const clientCoord = extractEventCoordinate(event, dir);
const positionValue = minmax(
clientCoord - chooseValueByDirection(dir, orig.x, orig.y),
0,
chooseValueByDirection(dir, dims.x, dims.y),
);
setPosition(positionValue);
};
},
[setPosition],
);
startSliderInteractionRef.current = (event: MouseEvent | TouchEvent) => {
const processSliderInteraction = useCallback(
(event: MouseEvent | TouchEvent) => {
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);
if (!isTouchEvent(event)) {
document.addEventListener(
"mousemove",
processSliderInteractionRef.current,
);
document.addEventListener("mouseup", endSliderInteractionRef.current, {
document.addEventListener("mousemove", processSliderInteraction);
document.addEventListener("mouseup", endSliderInteraction, {
passive: true,
});
} else {
document.addEventListener(
"touchmove",
processSliderInteractionRef.current,
);
document.addEventListener("touchend", endSliderInteractionRef.current, {
document.addEventListener("touchmove", processSliderInteraction);
document.addEventListener("touchend", endSliderInteraction, {
passive: true,
});
document.addEventListener("touchcancel", endSliderInteraction, {
passive: true,
});
document.addEventListener(
"touchcancel",
endSliderInteractionRef.current,
{
passive: true,
},
);
}
};
},
[calculatePosition, processSliderInteraction, endSliderInteraction],
);
processSliderInteractionRef.current = (event: MouseEvent | TouchEvent) => {
event.preventDefault();
calculatePositionRef.current(event);
};
// Setup scroll handlers
const handleScrollUp = useCallback(() => {
const dir = directionRef.current;
const dims = dimensionsRef.current;
endSliderInteractionRef.current = (event: MouseEvent | TouchEvent) => {
setIsDragging(false);
if (!isTouchEvent(event)) {
document.removeEventListener(
"mousemove",
processSliderInteractionRef.current,
);
document.removeEventListener(
"mouseup",
endSliderInteractionRef.current,
);
} else {
document.removeEventListener(
"touchmove",
processSliderInteractionRef.current,
);
document.removeEventListener(
"touchend",
endSliderInteractionRef.current,
);
document.removeEventListener(
"touchcancel",
endSliderInteractionRef.current,
);
}
};
}, []);
setPosition((prev: number) =>
minmax(prev - 1, 0, chooseValueByDirection(dir, dims.x, dims.y)),
);
}, [setPosition]);
const handleScrollDown = useCallback(() => {
const dir = directionRef.current;
const dims = dimensionsRef.current;
setPosition((prev: number) =>
minmax(prev + 1, 0, chooseValueByDirection(dir, dims.x, dims.y)),
);
}, [setPosition]);
const { addScrollListener, removeScrollListener } = useScroll({
targetRef: sliderRef,
onScrollUp: handleScrollUp,
onScrollDown: handleScrollDown,
});
// Set up entry listeners
useEffect(() => {
const currentRef = sliderRef.current;
if (currentRef) {
addScrollListener();
currentRef.addEventListener(
"mousedown",
startSliderInteractionRef.current,
);
currentRef.addEventListener(
"touchstart",
startSliderInteractionRef.current,
);
currentRef.addEventListener("mousedown", startSliderInteraction);
currentRef.addEventListener("touchstart", startSliderInteraction);
}
return () => {
if (currentRef) {
removeScrollListener();
currentRef.removeEventListener(
"mousedown",
startSliderInteractionRef.current,
);
currentRef.removeEventListener(
"touchstart",
startSliderInteractionRef.current,
);
currentRef.removeEventListener("mousedown", startSliderInteraction);
currentRef.removeEventListener("touchstart", startSliderInteraction);
}
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 };
}