From 46d89c94708b5bf9afa0e49c4f8c6cb2a3916061 Mon Sep 17 00:00:00 2001 From: Etan Joseph Heyman Date: Tue, 17 Feb 2026 04:09:50 +0200 Subject: [PATCH] nightshift: fix timeout memory leaks in dashboard Fix two uncleaned setTimeout calls in the dashboard that could cause state updates on unmounted components: 1. Migration modal timeout (500ms) now has proper cleanup 2. Goals initialization retry now uses a ref for cleanup on unmount, caps retries at 3 with increasing backoff (was unbounded/recursive) Co-Authored-By: Claude Opus 4.6 --- src/routes/_authed/dashboard.tsx | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/routes/_authed/dashboard.tsx b/src/routes/_authed/dashboard.tsx index 61a14fa..36dc2bc 100644 --- a/src/routes/_authed/dashboard.tsx +++ b/src/routes/_authed/dashboard.tsx @@ -4,7 +4,7 @@ import { convexQuery, useConvexMutation } from "@convex-dev/react-query"; import { api } from "@convex/_generated/api"; import { authClient } from "../../lib/auth-client"; import { Button } from "../../components/ui/button"; -import { useState, useCallback, useMemo, useEffect } from "react"; +import { useState, useCallback, useMemo, useEffect, useRef } from "react"; import type { Id } from "@convex/_generated/dataModel"; import WordDetailsModal from "../../components/WordDetailsModal"; import { getLanguageFlagString } from "../../components/LanguageFlag"; @@ -129,9 +129,10 @@ function DashboardPage() { setMigrationChecked(true); if (shouldShowMigrationModal(session.user.id)) { // Small delay to let dashboard render first - setTimeout(() => { + const timer = setTimeout(() => { setIsMigrationModalOpen(true); }, 500); + return () => clearTimeout(timer); } } }, [session?.user?.id, migrationChecked]); @@ -182,19 +183,35 @@ function DashboardPage() { const reorderWishlistMutation = useConvexMutation(api.wishlist.reorderWishlist); const removeFromWishlistMutation = useConvexMutation(api.wishlist.removeFromWishlist); - // Initialize default goals mutation with error handling and retry + // Track retry state for goals initialization + const goalsRetryRef = useRef({ count: 0, timerId: null as ReturnType | null }); + + // Clean up any pending retry on unmount + useEffect(() => { + return () => { + if (goalsRetryRef.current.timerId) { + clearTimeout(goalsRetryRef.current.timerId); + } + }; + }, []); + + // Initialize default goals mutation with error handling and capped retry const { mutate: initializeGoals } = useMutation({ mutationFn: (args: any) => initializeDefaultGoalsMutation(args), onSuccess: () => { + goalsRetryRef.current.count = 0; refetchGoals(); }, onError: (error) => { console.warn('Initialize goals failed:', error); - // Retry after a delay if authentication error - if (error.message?.includes('Authentication required')) { - setTimeout(() => { + // Retry up to 3 times with increasing delay if authentication error + if (error.message?.includes('Authentication required') && goalsRetryRef.current.count < 3) { + goalsRetryRef.current.count++; + const delay = 2000 * goalsRetryRef.current.count; + goalsRetryRef.current.timerId = setTimeout(() => { + goalsRetryRef.current.timerId = null; initializeGoals({}); - }, 2000); + }, delay); } }, });