diff --git a/src/hooks/crosshair.tsx b/src/hooks/crosshair.tsx index bed322b..eea0f4a 100644 --- a/src/hooks/crosshair.tsx +++ b/src/hooks/crosshair.tsx @@ -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 { CartesianSpace } from "../types"; import { minmax } from "../util"; @@ -12,6 +12,22 @@ function isTouchEvent(event: Event): event is TouchEvent { 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({ origin, dimensions, @@ -26,19 +42,6 @@ export function useCrosshair({ const [isDragging, setIsDragging] = useState(false); const crosshairRef = useRef(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 dimensionsRef = useRef(dimensions); @@ -47,121 +50,92 @@ export function useCrosshair({ dimensionsRef.current = dimensions; }, [origin, dimensions]); - // Update handler functions when dependencies change via reference - - useEffect(() => { - calculatePositionsRef.current = (event: MouseEvent | TouchEvent) => { + const calculatePositions = useCallback( + (event: MouseEvent | TouchEvent) => { const orig = originRef.current; const dims = dimensionsRef.current; - const clientX = isTouchEvent(event) - ? event.touches[0].clientX - : event.clientX; - const clientY = isTouchEvent(event) - ? event.touches[0].clientY - : event.clientY; + const { clientX, clientY } = extractEventCoordinates(event); const xPos = minmax(clientX - orig.x, 0, dims.x - 1); const yPos = minmax(clientY - orig.y, 0, dims.y - 1); setXPosition(xPos); setYPosition(yPos); - }; + }, + [setXPosition, setYPosition], + ); - startCrosshairInteractionRef.current = (event: MouseEvent | TouchEvent) => { + const processCrosshairInteraction = useCallback( + (event: MouseEvent | TouchEvent) => { 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); if (!isTouchEvent(event)) { - document.addEventListener( - "mousemove", - processCrosshairInteractionRef.current, - ); - document.addEventListener( - "mouseup", - endCrosshairInteractionRef.current, - { passive: true }, - ); + document.addEventListener("mousemove", processCrosshairInteraction); + document.addEventListener("mouseup", endCrosshairInteraction, { + passive: true, + }); } else { - document.addEventListener( - "touchmove", - processCrosshairInteractionRef.current, - ); - document.addEventListener( - "touchend", - endCrosshairInteractionRef.current, - { passive: true }, - ); - document.addEventListener( - "touchcancel", - endCrosshairInteractionRef.current, - { passive: true }, - ); + document.addEventListener("touchmove", processCrosshairInteraction); + document.addEventListener("touchend", endCrosshairInteraction, { + passive: true, + }); + document.addEventListener("touchcancel", endCrosshairInteraction, { + 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(() => { const currentRef = crosshairRef.current; if (currentRef) { - currentRef.addEventListener( - "mousedown", - startCrosshairInteractionRef.current, - ); - currentRef.addEventListener( - "touchstart", - startCrosshairInteractionRef.current, - ); + currentRef.addEventListener("mousedown", startCrosshairInteraction); + currentRef.addEventListener("touchstart", startCrosshairInteraction); } return () => { if (currentRef) { - currentRef.removeEventListener( - "mousedown", - startCrosshairInteractionRef.current, - ); - currentRef.removeEventListener( - "touchstart", - startCrosshairInteractionRef.current, - ); + currentRef.removeEventListener("mousedown", startCrosshairInteraction); + currentRef.removeEventListener("touchstart", startCrosshairInteraction); } + + 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 }; } diff --git a/src/hooks/scroll.ts b/src/hooks/scroll.ts index e009e1e..4e40c18 100644 --- a/src/hooks/scroll.ts +++ b/src/hooks/scroll.ts @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from "react"; +import { useState, useRef, useEffect, useCallback } from "react"; import type { RefObject } from "react"; export function handleScroll( @@ -6,7 +6,7 @@ export function handleScroll( scrollDelta: number, handleIncrement: () => void, handleDecrement: () => void, -) { +): number { const newLength = prevLength + scrollDelta; if (Math.abs(newLength) > 50) { @@ -47,42 +47,38 @@ export function useScroll({ deltaYMultiplierRef.current = deltaYMultiplier; }, [onScrollUp, onScrollDown, deltaYMultiplier]); - const handleWheelEventRef = useRef((_: WheelEvent) => {}); + const handleWheelEvent = useCallback((event: WheelEvent) => { + event.preventDefault(); - useEffect(() => { - handleWheelEventRef.current = (event: WheelEvent) => { - event.preventDefault(); - - setScrollLength((prev) => - handleScroll( - prev, - event.deltaY * deltaYMultiplierRef.current, - onScrollDownRef.current, - onScrollUpRef.current, - ), - ); - }; + setScrollLength((prev) => + handleScroll( + prev, + event.deltaY * deltaYMultiplierRef.current, + onScrollDownRef.current, + onScrollUpRef.current, + ), + ); }, []); - function addScrollListener() { + const addScrollListener = useCallback(() => { const currentRef = targetRef.current; if (currentRef) { - currentRef.addEventListener("wheel", handleWheelEventRef.current); + currentRef.addEventListener("wheel", handleWheelEvent); } - } + }, [handleWheelEvent, targetRef]); - function removeScrollListener() { + const removeScrollListener = useCallback(() => { const currentRef = targetRef.current; if (currentRef) { - currentRef.removeEventListener("wheel", handleWheelEventRef.current); + currentRef.removeEventListener("wheel", handleWheelEvent); } - } + }, [handleWheelEvent, targetRef]); useEffect(() => { return () => { removeScrollListener(); }; - }, []); + }, [removeScrollListener]); return { addScrollListener, diff --git a/src/hooks/slider.tsx b/src/hooks/slider.tsx index 13d9055..7634c3e 100644 --- a/src/hooks/slider.tsx +++ b/src/hooks/slider.tsx @@ -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>; -}): { sliderRef: RefObject; isDragging: boolean } { +}) { const [isDragging, setIsDragging] = useState(false); const sliderRef = useRef(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 }; }