From 3b33a3efdb5e879f7f5ce8ce63069fb0a89dbd36 Mon Sep 17 00:00:00 2001 From: qorexdev Date: Sat, 28 Mar 2026 04:01:49 +0500 Subject: [PATCH 1/2] refactor: move onboarding animation state into event handlers Remove useEffect that watched stepIndex to drive shouldAnimate/contentKey. stepIndex only changes from handleNext/handlePrev, so set shouldAnimate directly in those handlers. Replace contentKey state (always equal to stepIndex) with stepIndex as the animation key. Fixes #1017 --- surfsense_web/components/onboarding-tour.tsx | 26 +++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/surfsense_web/components/onboarding-tour.tsx b/surfsense_web/components/onboarding-tour.tsx index 03fad87b6..e097c212d 100644 --- a/surfsense_web/components/onboarding-tour.tsx +++ b/surfsense_web/components/onboarding-tour.tsx @@ -160,6 +160,8 @@ function TourTooltip({ onPrev, onSkip, isDarkMode, + shouldAnimate, + onAnimationEnd, }: { step: TourStep; stepIndex: number; @@ -170,23 +172,12 @@ function TourTooltip({ onPrev: () => void; onSkip: () => void; isDarkMode: boolean; + shouldAnimate: boolean; + onAnimationEnd: () => void; }) { - const [contentKey, setContentKey] = useState(stepIndex); - const [shouldAnimate, setShouldAnimate] = useState(false); - const prevStepIndexRef = useRef(stepIndex); const isLastStep = stepIndex === totalSteps - 1; const isFirstStep = stepIndex === 0; - // Update content key when step changes to trigger animation - // Only animate if stepIndex actually changes (not on initial mount) - useEffect(() => { - if (prevStepIndexRef.current !== stepIndex) { - setShouldAnimate(true); - setContentKey(stepIndex); - prevStepIndexRef.current = stepIndex; - } - }, [stepIndex]); - const bgColor = isDarkMode ? "#27272a" : "#ffffff"; const textColor = isDarkMode ? "#ffffff" : "#18181b"; const mutedTextColor = isDarkMode ? "#a1a1aa" : "#71717a"; @@ -358,11 +349,11 @@ function TourTooltip({ > {/* Content */}
setShouldAnimate(false)} + onAnimationEnd={onAnimationEnd} >

{step.title} @@ -427,6 +418,7 @@ export function OnboardingTour() { const isMobile = useIsMobile(); const [isActive, setIsActive] = useState(false); const [stepIndex, setStepIndex] = useState(0); + const [shouldAnimate, setShouldAnimate] = useState(false); const [targetEl, setTargetEl] = useState(null); const [spotlightTargetEl, setSpotlightTargetEl] = useState(null); const [spotlightStepTarget, setSpotlightStepTarget] = useState(null); @@ -664,6 +656,7 @@ export function OnboardingTour() { const handleNext = useCallback(() => { if (stepIndex < TOUR_STEPS.length - 1) { retryCountRef.current = 0; + setShouldAnimate(true); setStepIndex(stepIndex + 1); } else { // Tour completed - save to localStorage @@ -678,6 +671,7 @@ export function OnboardingTour() { const handlePrev = useCallback(() => { if (stepIndex > 0) { retryCountRef.current = 0; + setShouldAnimate(true); setStepIndex(stepIndex - 1); } }, [stepIndex]); @@ -772,6 +766,8 @@ export function OnboardingTour() { onPrev={handlePrev} onSkip={handleSkip} isDarkMode={isDarkMode} + shouldAnimate={shouldAnimate} + onAnimationEnd={() => setShouldAnimate(false)} /> )} From 6540e3f774d01657f0cac359310cd827dafc7c07 Mon Sep 17 00:00:00 2001 From: qorexdev Date: Sat, 28 Mar 2026 15:28:50 +0500 Subject: [PATCH 2/2] refactor: wrap onAnimationEnd in useCallback for stable reference --- surfsense_web/components/onboarding-tour.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/surfsense_web/components/onboarding-tour.tsx b/surfsense_web/components/onboarding-tour.tsx index e097c212d..4204928ce 100644 --- a/surfsense_web/components/onboarding-tour.tsx +++ b/surfsense_web/components/onboarding-tour.tsx @@ -695,6 +695,10 @@ export function OnboardingTour() { setIsActive(false); }, [user?.id]); + const handleAnimationEnd = useCallback(() => { + setShouldAnimate(false); + }, []); + // Handle escape key useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -767,7 +771,7 @@ export function OnboardingTour() { onSkip={handleSkip} isDarkMode={isDarkMode} shouldAnimate={shouldAnimate} - onAnimationEnd={() => setShouldAnimate(false)} + onAnimationEnd={handleAnimationEnd} /> )}