fix: resolve stale closures in CandleChart click handler
Convert drawingState, selectedLineId, dragState, and annotations to refs that stay in sync with state via useEffect. The chart click handler (subscribeClick) captured stale closure values for these variables. Now reads from refs (e.g. drawingStateRef.current) so the handler always sees the latest state, and removed the stale state variables from the useEffect dependency array. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
45a23047dd
commit
73c88f8211
2 changed files with 35 additions and 22 deletions
|
|
@ -151,6 +151,18 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
|
|||
const [selectedLineId, setSelectedLineId] = useState<number | null>(null);
|
||||
const [dragState, setDragState] = useState<{ lineId: number; endpoint: 'p1' | 'p2' } | null>(null);
|
||||
|
||||
// Refs to avoid stale closures in chart click/mouse handlers
|
||||
const drawingStateRef = useRef<DrawingState | null>(drawingState);
|
||||
const selectedLineIdRef = useRef<number | null>(selectedLineId);
|
||||
const dragStateRef = useRef<{ lineId: number; endpoint: 'p1' | 'p2' } | null>(dragState);
|
||||
const annotationsRef = useRef<Annotation[]>(annotations);
|
||||
|
||||
// Keep refs in sync with state
|
||||
useEffect(() => { drawingStateRef.current = drawingState; }, [drawingState]);
|
||||
useEffect(() => { selectedLineIdRef.current = selectedLineId; }, [selectedLineId]);
|
||||
useEffect(() => { dragStateRef.current = dragState; }, [dragState]);
|
||||
useEffect(() => { annotationsRef.current = annotations; }, [annotations]);
|
||||
|
||||
// Track mounted state to avoid hydration mismatch
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
|
|
@ -584,29 +596,29 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
|
|||
if (time === null || price === null) return;
|
||||
|
||||
// Check if clicking on a selected line's endpoint handle to start dragging
|
||||
if (selectedLineId !== null && !activeTool) {
|
||||
const linePrimitive = linePrimitivesRef.current.get(selectedLineId);
|
||||
if (selectedLineIdRef.current !== null && !activeTool) {
|
||||
const linePrimitive = linePrimitivesRef.current.get(selectedLineIdRef.current);
|
||||
if (linePrimitive) {
|
||||
const p1 = linePrimitive.getP1();
|
||||
const p2 = linePrimitive.getP2();
|
||||
|
||||
if (isNearEndpoint(timeCoordinate, priceCoordinate, p1.time, p1.price)) {
|
||||
setDragState({ lineId: selectedLineId, endpoint: 'p1' });
|
||||
setDragState({ lineId: selectedLineIdRef.current!, endpoint: 'p1' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNearEndpoint(timeCoordinate, priceCoordinate, p2.time, p2.price)) {
|
||||
setDragState({ lineId: selectedLineId, endpoint: 'p2' });
|
||||
setDragState({ lineId: selectedLineIdRef.current!, endpoint: 'p2' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If currently dragging, complete the drag operation
|
||||
if (dragState) {
|
||||
const linePrimitive = linePrimitivesRef.current.get(dragState.lineId);
|
||||
if (dragStateRef.current) {
|
||||
const linePrimitive = linePrimitivesRef.current.get(dragStateRef.current.lineId);
|
||||
if (linePrimitive) {
|
||||
const annotation = annotations.find(a => a.id === dragState.lineId);
|
||||
const annotation = annotationsRef.current.find(a => a.id === dragStateRef.current!.lineId);
|
||||
if (annotation) {
|
||||
// Persist the updated geometry
|
||||
const p1 = linePrimitive.getP1();
|
||||
|
|
@ -638,7 +650,7 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
|
|||
|
||||
// Handle line and rectangle drawing
|
||||
if (activeTool === 'line' || activeTool === 'rectangle') {
|
||||
if (!drawingState) {
|
||||
if (!drawingStateRef.current) {
|
||||
// First click: record first point and show preview
|
||||
const firstPoint = { time, price };
|
||||
setDrawingState({ tool: activeTool, firstPoint });
|
||||
|
|
@ -678,10 +690,11 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
|
|||
const secondPoint = { time, price };
|
||||
|
||||
try {
|
||||
const timestamp = typeof drawingState.firstPoint.time === 'string'
|
||||
? Date.parse(drawingState.firstPoint.time) / 1000
|
||||
: (drawingState.firstPoint.time as number);
|
||||
|
||||
const currentDrawing = drawingStateRef.current!;
|
||||
const timestamp = typeof currentDrawing.firstPoint.time === 'string'
|
||||
? Date.parse(currentDrawing.firstPoint.time) / 1000
|
||||
: (currentDrawing.firstPoint.time as number);
|
||||
|
||||
const response = await fetch('/api/annotations', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
|
@ -691,10 +704,10 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
|
|||
chart_id: activeChartId,
|
||||
color: selectedColor,
|
||||
geometry: {
|
||||
startTime: typeof drawingState.firstPoint.time === 'string'
|
||||
? Date.parse(drawingState.firstPoint.time) / 1000
|
||||
: drawingState.firstPoint.time,
|
||||
startPrice: drawingState.firstPoint.price,
|
||||
startTime: typeof currentDrawing.firstPoint.time === 'string'
|
||||
? Date.parse(currentDrawing.firstPoint.time) / 1000
|
||||
: currentDrawing.firstPoint.time,
|
||||
startPrice: currentDrawing.firstPoint.price,
|
||||
endTime: typeof secondPoint.time === 'string'
|
||||
? Date.parse(secondPoint.time) / 1000
|
||||
: secondPoint.time,
|
||||
|
|
@ -788,7 +801,7 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
|
|||
if (response.ok) {
|
||||
seriesRef.current!.detachPrimitive(deleteLineHit.primitive);
|
||||
linePrimitivesRef.current.delete(deleteLineHit.id);
|
||||
if (selectedLineId === deleteLineHit.id) {
|
||||
if (selectedLineIdRef.current === deleteLineHit.id) {
|
||||
setSelectedLineId(null);
|
||||
}
|
||||
await fetchAnnotations();
|
||||
|
|
@ -838,7 +851,7 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
|
|||
|
||||
// Find annotation at this timestamp (within tolerance)
|
||||
const tolerance = 60; // 60 seconds tolerance
|
||||
const annotation = annotations.find(
|
||||
const annotation = annotationsRef.current.find(
|
||||
(a) =>
|
||||
markerTypeNames.includes(a.label_type) &&
|
||||
Math.abs(a.timestamp - timestamp) < tolerance
|
||||
|
|
@ -876,7 +889,7 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
|
|||
|
||||
if (lineHit && activeTool !== 'delete') {
|
||||
// Toggle selection
|
||||
const newSelectedId = selectedLineId === lineHit.id ? null : lineHit.id;
|
||||
const newSelectedId = selectedLineIdRef.current === lineHit.id ? null : lineHit.id;
|
||||
setSelectedLineId(newSelectedId);
|
||||
|
||||
// Update all lines' selection state
|
||||
|
|
@ -955,7 +968,7 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
|
|||
|
||||
// Find annotation at this timestamp (within tolerance)
|
||||
const tolerance = 60; // 60 seconds tolerance
|
||||
const annotation = annotations.find(
|
||||
const annotation = annotationsRef.current.find(
|
||||
(a) =>
|
||||
markerTypeNames.includes(a.label_type) &&
|
||||
Math.abs(a.timestamp - timestamp) < tolerance
|
||||
|
|
@ -972,7 +985,7 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
|
|||
return () => {
|
||||
chartRef.current?.unsubscribeClick(handleClick);
|
||||
};
|
||||
}, [activeTool, candles, annotations, annotationTypes, onAnnotationChange, predictionVisible, predictionSpans, predictionSummary, onPredictionClick, onPredictionDismiss, drawingState, selectedColor]);
|
||||
}, [activeTool, candles, annotationTypes, onAnnotationChange, predictionVisible, predictionSpans, predictionSummary, onPredictionClick, onPredictionDismiss, selectedColor]);
|
||||
|
||||
// Handle crosshair move to update preview during drawing and dragging
|
||||
useEffect(() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue