fix: clean up onboarding tour timer leaks

Fix two timer cleanup bugs in onboarding-tour.tsx:

1. Remove cleanup return from useCallback (only works in useEffect).
   Clear retryTimerRef at the start of updateTarget and in a
   dedicated useEffect cleanup instead.

2. Track recursive setTimeout calls via startCheckTimerRef so they
   are properly cancelled on unmount instead of leaking.

Fixes #1091
This commit is contained in:
Matt Van Horn 2026-04-02 23:59:15 -07:00
parent c1c4c534c0
commit b9b2bac16f

View file

@ -429,6 +429,7 @@ export function OnboardingTour() {
const pathname = usePathname(); const pathname = usePathname();
const retryCountRef = useRef(0); const retryCountRef = useRef(0);
const retryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); const retryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const startCheckTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const maxRetries = 10; const maxRetries = 10;
// Track previous user ID to detect user changes // Track previous user ID to detect user changes
const previousUserIdRef = useRef<string | null>(null); const previousUserIdRef = useRef<string | null>(null);
@ -460,6 +461,7 @@ export function OnboardingTour() {
// Find and track target element with retry logic // Find and track target element with retry logic
const updateTarget = useCallback(() => { const updateTarget = useCallback(() => {
if (retryTimerRef.current) clearTimeout(retryTimerRef.current);
if (!currentStep) return; if (!currentStep) return;
const el = document.querySelector(currentStep.target); const el = document.querySelector(currentStep.target);
@ -480,11 +482,13 @@ export function OnboardingTour() {
} }
}, 200); }, 200);
} }
}, [currentStep]);
useEffect(() => {
return () => { return () => {
if (retryTimerRef.current) clearTimeout(retryTimerRef.current); if (retryTimerRef.current) clearTimeout(retryTimerRef.current);
}; };
}, [currentStep]); }, []);
// Check if tour should run: localStorage + data validation with user ID tracking // Check if tour should run: localStorage + data validation with user ID tracking
useEffect(() => { useEffect(() => {
@ -573,15 +577,15 @@ export function OnboardingTour() {
setPosition(calculatePosition(connectorEl, TOUR_STEPS[0].placement)); setPosition(calculatePosition(connectorEl, TOUR_STEPS[0].placement));
} else { } else {
// Retry after delay // Retry after delay
setTimeout(checkAndStartTour, 200); startCheckTimerRef.current = setTimeout(checkAndStartTour, 200);
} }
}; };
// Start checking after initial delay // Start checking after initial delay
const timer = setTimeout(checkAndStartTour, 500); startCheckTimerRef.current = setTimeout(checkAndStartTour, 500);
return () => { return () => {
cancelled = true; cancelled = true;
clearTimeout(timer); if (startCheckTimerRef.current) clearTimeout(startCheckTimerRef.current);
}; };
}, [mounted, user?.id, searchSpaceId, pathname, threadsData, documentTypeCounts, connectors]); }, [mounted, user?.id, searchSpaceId, pathname, threadsData, documentTypeCounts, connectors]);