From 748b0514ea6cba6424cd3054d93c201693ee7344 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Tue, 26 May 2026 15:40:03 +0300 Subject: [PATCH] fix(telegram-redirect): cancel pending timers on effect re-run/unmount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TelegramRedirect schedules up to four setTimeouts (loading-screen-delay, already-authenticated-redirect, not-in-telegram-redirect, post-login redirect) inside a useEffect whose deps include isAuthenticated / authLoading — both of which change during loginWithTelegram, re-running the effect mid-flight. None of the timers were tracked, so the previous run's pending navigate() callbacks fired after the new run started, double-triggering setState on the new closure. Route every schedule through a local timers[] that the effect cleanup flushes. Same UX on the happy path; no stray late navigations on the re-entrancy edge. --- src/pages/TelegramRedirect.tsx | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/pages/TelegramRedirect.tsx b/src/pages/TelegramRedirect.tsx index 5e20bbc..2ab30c5 100644 --- a/src/pages/TelegramRedirect.tsx +++ b/src/pages/TelegramRedirect.tsx @@ -68,11 +68,20 @@ export default function TelegramRedirect() { const redirectTo = getSafeRedirectUrl(searchParams.get('redirect')); useEffect(() => { + // All timers scheduled inside this effect funnel through `timers` so the + // cleanup can cancel everything when the effect re-runs (deps change + // during loginWithTelegram) or the page unmounts — preventing + // setState-on-unmounted-component warnings and stray late navigations. + const timers: ReturnType[] = []; + const schedule = (fn: () => void, ms: number) => { + timers.push(setTimeout(fn, ms)); + }; + // If already authenticated, redirect immediately if (isAuthenticated && !authLoading) { setStatus('success'); - setTimeout(() => navigate(redirectTo), 500); - return; + schedule(() => navigate(redirectTo), 500); + return () => timers.forEach(clearTimeout); } const initTelegram = async () => { @@ -82,7 +91,7 @@ export default function TelegramRedirect() { if (!isInTelegramWebApp() || !initData) { // Not in Telegram, show message and redirect to login setStatus('not-telegram'); - setTimeout(() => navigate('/login'), 2000); + schedule(() => navigate('/login'), 2000); return; } @@ -91,11 +100,8 @@ export default function TelegramRedirect() { try { await loginWithTelegram(initData); setStatus('success'); - // Small delay for nice UX - setTimeout(() => { - navigate(redirectTo); - }, 800); + schedule(() => navigate(redirectTo), 800); } catch (err: unknown) { console.error('Telegram auth failed:', err); const error = err as { response?: { data?: { detail?: string } } }; @@ -105,7 +111,9 @@ export default function TelegramRedirect() { }; // Small delay to show loading screen - setTimeout(initTelegram, 300); + schedule(initTelegram, 300); + + return () => timers.forEach(clearTimeout); }, [loginWithTelegram, navigate, isAuthenticated, authLoading, redirectTo, t]); // Handle retry with limit to prevent infinite loops