mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: prevent stale closure in pipeline step callbacks
All step mutation callbacks (addStep, removeStep, moveStep, updateStepSettings) captured `steps` in their useCallback closures. When React batched state updates, rapid interactions could use a stale steps array, causing clicks to silently fail. Fix: use a stepsRef that always holds the latest value. Callbacks read from stepsRef.current instead of the captured closure variable, and no longer need `steps` in their dependency arrays.
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
|||||||
Upload,
|
Upload,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { SearchBar } from "@/components/common/search-bar";
|
import { SearchBar } from "@/components/common/search-bar";
|
||||||
import { apiGet } from "@/lib/api";
|
import { apiGet } from "@/lib/api";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -67,6 +67,13 @@ export function PipelineBuilder({
|
|||||||
const [pipelineToolIds, setPipelineToolIds] = useState<string[] | null>(null);
|
const [pipelineToolIds, setPipelineToolIds] = useState<string[] | null>(null);
|
||||||
const [toolSearch, setToolSearch] = useState("");
|
const [toolSearch, setToolSearch] = useState("");
|
||||||
|
|
||||||
|
// Keep a ref to steps so callbacks always read the latest value
|
||||||
|
// without needing steps in their dependency arrays (prevents stale closures).
|
||||||
|
const stepsRef = useRef(steps);
|
||||||
|
useEffect(() => {
|
||||||
|
stepsRef.current = steps;
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
apiGet<{ settings: Record<string, string> }>("/v1/settings")
|
apiGet<{ settings: Record<string, string> }>("/v1/settings")
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@@ -105,40 +112,43 @@ export function PipelineBuilder({
|
|||||||
toolId,
|
toolId,
|
||||||
settings: {},
|
settings: {},
|
||||||
};
|
};
|
||||||
onStepsChange([...steps, step]);
|
onStepsChange([...stepsRef.current, step]);
|
||||||
setShowToolPicker(false);
|
setShowToolPicker(false);
|
||||||
setToolSearch("");
|
setToolSearch("");
|
||||||
setExpandedStep(step.id);
|
setExpandedStep(step.id);
|
||||||
},
|
},
|
||||||
[steps, onStepsChange],
|
[onStepsChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const removeStep = useCallback(
|
const removeStep = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
onStepsChange(steps.filter((s) => s.id !== id));
|
onStepsChange(stepsRef.current.filter((s) => s.id !== id));
|
||||||
if (expandedStep === id) setExpandedStep(null);
|
setExpandedStep((prev) => (prev === id ? null : prev));
|
||||||
},
|
},
|
||||||
[steps, onStepsChange, expandedStep],
|
[onStepsChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const moveStep = useCallback(
|
const moveStep = useCallback(
|
||||||
(id: string, direction: "up" | "down") => {
|
(id: string, direction: "up" | "down") => {
|
||||||
const idx = steps.findIndex((s) => s.id === id);
|
const current = stepsRef.current;
|
||||||
|
const idx = current.findIndex((s) => s.id === id);
|
||||||
if (idx < 0) return;
|
if (idx < 0) return;
|
||||||
const newIdx = direction === "up" ? idx - 1 : idx + 1;
|
const newIdx = direction === "up" ? idx - 1 : idx + 1;
|
||||||
if (newIdx < 0 || newIdx >= steps.length) return;
|
if (newIdx < 0 || newIdx >= current.length) return;
|
||||||
const newSteps = [...steps];
|
const newSteps = [...current];
|
||||||
[newSteps[idx], newSteps[newIdx]] = [newSteps[newIdx], newSteps[idx]];
|
[newSteps[idx], newSteps[newIdx]] = [newSteps[newIdx], newSteps[idx]];
|
||||||
onStepsChange(newSteps);
|
onStepsChange(newSteps);
|
||||||
},
|
},
|
||||||
[steps, onStepsChange],
|
[onStepsChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateStepSettings = useCallback(
|
const updateStepSettings = useCallback(
|
||||||
(id: string, newSettings: Record<string, unknown>) => {
|
(id: string, newSettings: Record<string, unknown>) => {
|
||||||
onStepsChange(steps.map((s) => (s.id === id ? { ...s, settings: newSettings } : s)));
|
onStepsChange(
|
||||||
|
stepsRef.current.map((s) => (s.id === id ? { ...s, settings: newSettings } : s)),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[steps, onStepsChange],
|
[onStepsChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFileSelect = useCallback(() => {
|
const handleFileSelect = useCallback(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user