fix: clean up recursive setTimeout calls in onboarding tour

- Add cancelled flag to prevent state updates after unmount in
  checkAndStartTour retry loop
- Store retry timer ID in a ref and clear it on cleanup in
  updateTarget effect

Closes #950
This commit is contained in:
likiosliu 2026-03-25 12:34:30 +08:00
parent a474c4651c
commit e5cabf95e4

View file

@ -436,6 +436,7 @@ export function OnboardingTour() {
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
const pathname = usePathname(); const pathname = usePathname();
const retryCountRef = useRef(0); const retryCountRef = useRef(0);
const retryTimerRef = 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);
@ -477,7 +478,7 @@ export function OnboardingTour() {
retryCountRef.current = 0; retryCountRef.current = 0;
} else if (retryCountRef.current < maxRetries) { } else if (retryCountRef.current < maxRetries) {
retryCountRef.current++; retryCountRef.current++;
setTimeout(() => { retryTimerRef.current = setTimeout(() => {
const retryEl = document.querySelector(currentStep.target); const retryEl = document.querySelector(currentStep.target);
if (retryEl) { if (retryEl) {
setTargetEl(retryEl); setTargetEl(retryEl);
@ -487,6 +488,10 @@ export function OnboardingTour() {
} }
}, 200); }, 200);
} }
return () => {
if (retryTimerRef.current) clearTimeout(retryTimerRef.current);
};
}, [currentStep]); }, [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
@ -556,7 +561,11 @@ export function OnboardingTour() {
} }
// User is new and hasn't seen tour - wait for DOM elements and start tour // User is new and hasn't seen tour - wait for DOM elements and start tour
let cancelled = false;
const checkAndStartTour = () => { const checkAndStartTour = () => {
if (cancelled) return;
// Check if all required elements exist // Check if all required elements exist
const connectorEl = document.querySelector(TOUR_STEPS[0].target); const connectorEl = document.querySelector(TOUR_STEPS[0].target);
const documentsEl = document.querySelector(TOUR_STEPS[1].target); const documentsEl = document.querySelector(TOUR_STEPS[1].target);
@ -578,7 +587,10 @@ export function OnboardingTour() {
// Start checking after initial delay // Start checking after initial delay
const timer = setTimeout(checkAndStartTour, 500); const timer = setTimeout(checkAndStartTour, 500);
return () => clearTimeout(timer); return () => {
cancelled = true;
clearTimeout(timer);
};
}, [mounted, user?.id, searchSpaceId, pathname, threadsData, documentTypeCounts, connectors]); }, [mounted, user?.id, searchSpaceId, pathname, threadsData, documentTypeCounts, connectors]);
// Update position on resize/scroll // Update position on resize/scroll