mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-09-01 04:00:28 +00:00
fix(explorer): resolve #830 — Maximum update depth exceeded on Temporal panel open
Two independent render loops were causing the Temporal panel to remain stuck on 'Loading temporal...' in npm run dev: Loop 1 — diagnostics state churn (GraphWorkspace.tsx): handleDiagnosticsChange unconditionally called setGraphDiagnosticsState with a new object on every invocation. buildEffectAvailability (called inside GraphCanvas's diagnostics useEffect) always returns a new object, so setGraphDiagnosticsState was called on every effect run, creating a cycle: setGraphDiagnosticsState graphDiagnosticsState new diagnosticsSnapshot new pluginContext new handleInteractionStateChange new GraphCanvas re-renders diagnostics effect fires again. Fix: before calling setGraphDiagnosticsState, compare the incoming diagnostics field-by-field against the last accepted snapshot via a ref (lastDiagnosticsRef). All effectAvailability entries, edgeClasses.updatedAt, structureLayer.cacheKey/lastDrawAt/enabled, and distanceVisual identity must differ for a state update to proceed. The ref approach avoids scheduling a re-render at all, rather than bailing out inside a functional updater after the render has already been committed. Loop 2 — scrubberTime churn (GraphWorkspace.tsx + GraphWorkspaceShell.tsx): TimelinePanel.tsx calls onTimeChange(defaultTime) whenever its useEffect re-runs. React 18 concurrent mode re-runs effects with structurally-new Date objects for the same timestamp when speculative renders discard useMemo caches, causing setScrubberTime to be called repeatedly with a new Date that has the same millisecond value — triggering temporalState churn, the diagnostics effect, and eventually the same loop. Fix: wrap setScrubberTime in an onTimeChange useCallback that compares the incoming time's millisecond value against the last sent value (via lastScrubberMsRef). Redundant calls with the same timestamp are dropped before reaching setScrubberTime. Stable useCallback identity also prevents TimelinePanel's useEffect from re-firing solely due to prop identity churn. Both fixes applied to GraphWorkspace.tsx and identically to GraphWorkspaceShell.tsx which has the same pattern. Verified: - npm run dev: 0 'Maximum update depth exceeded' errors - Temporal panel renders with real data in dev mode - Effects and Neighbors panels unaffected - npm run build + preview: identical behavior, 0 errors - All 42 frontend tests pass (34 graph-workspace, 1 graph-store, 7 plugin-registry)
This commit is contained in:
@@ -325,6 +325,17 @@ export function GraphWorkspaceShell() {
|
||||
const [activeNodeCount, setActiveNodeCount] = useState<number | null>(null);
|
||||
const [temporalBounds, setTemporalBounds] = useState<TemporalBounds | null>(null);
|
||||
const [scrubberTime, setScrubberTime] = useState<Date | null>(null);
|
||||
// Deduplicates setScrubberTime calls by millisecond value — same fix as
|
||||
// GraphWorkspace.tsx (issue #830).
|
||||
const lastScrubberMsRef = useRef<number | null>(null);
|
||||
const onTimeChange = useCallback((time: Date) => {
|
||||
const ms = time.getTime();
|
||||
if (ms === lastScrubberMsRef.current) {
|
||||
return;
|
||||
}
|
||||
lastScrubberMsRef.current = ms;
|
||||
setScrubberTime(time);
|
||||
}, []);
|
||||
const [loadingProgress, setLoadingProgress] = useState<GraphLoadProgress | null>(null);
|
||||
const [isGraphStageReady, setIsGraphStageReady] = useState(false);
|
||||
const [layoutStatus, setLayoutStatus] = useState<GraphLayoutStatus>({
|
||||
@@ -632,7 +643,7 @@ export function GraphWorkspaceShell() {
|
||||
|
||||
<Suspense fallback={<TimelineFallback min={temporalBounds?.min ?? null} max={temporalBounds?.max ?? null} />}>
|
||||
<TimelinePanel
|
||||
onTimeChange={setScrubberTime}
|
||||
onTimeChange={onTimeChange}
|
||||
minDate={temporalBounds?.min ?? undefined}
|
||||
maxDate={temporalBounds?.max ?? undefined}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user