From 1a8c4a9c1da84a262e8459a314e1eaa694b274c1 Mon Sep 17 00:00:00 2001 From: Nitesh Agarwal Date: Sat, 13 Jun 2026 22:14:44 +0530 Subject: [PATCH 1/2] fix: resolve cascading renders and optimistic UI race conditions in useLearningProgress --- src/hooks/useLearningProgress.ts | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/hooks/useLearningProgress.ts b/src/hooks/useLearningProgress.ts index 0f221e42..fcae2b63 100644 --- a/src/hooks/useLearningProgress.ts +++ b/src/hooks/useLearningProgress.ts @@ -19,12 +19,9 @@ export function useLearningProgress(): UseLearningProgressResult { useEffect(() => { if (!user) { - setCompletedNodes([]); - setLoading(false); return; } - setLoading(true); const docRef = doc(db, 'user_progress', user.uid); const unsubscribe = onSnapshot( docRef, @@ -46,20 +43,23 @@ export function useLearningProgress(): UseLearningProgressResult { return () => unsubscribe(); }, [user]); + // Derive state based on user authentication status to avoid cascading renders + const actualCompletedNodes = user ? completedNodes : []; + const actualLoading = user ? loading : false; + const toggleNode = async (pathId: string, nodeId: string) => { if (!user) return; const nodeKey = `${pathId}-${nodeId}`; - const isCompleted = completedNodes.includes(nodeKey); + const isCompleted = actualCompletedNodes.includes(nodeKey); - // Optimistic UI Update: Reflect state immediately in local array - const previousCompletedNodes = [...completedNodes]; const nextCompletedNodes = isCompleted - ? completedNodes.filter((id) => id !== nodeKey) - : [...completedNodes, nodeKey]; - - setCompletedNodes(nextCompletedNodes); + ? actualCompletedNodes.filter((id) => id !== nodeKey) + : [...actualCompletedNodes, nodeKey]; + // Rely exclusively on Firestore's native latency compensation (which instantly triggers + // the onSnapshot listener locally) instead of manually maintaining optimistic state + // which leads to race conditions. try { const docRef = doc(db, 'user_progress', user.uid); await setDoc( @@ -73,18 +73,16 @@ export function useLearningProgress(): UseLearningProgressResult { ); } catch (error) { console.error("Failed to save learning progress:", error); - // Revert state on error if background write fails - setCompletedNodes(previousCompletedNodes); } }; const isNodeCompleted = (pathId: string, nodeId: string) => { - return completedNodes.includes(`${pathId}-${nodeId}`); + return actualCompletedNodes.includes(`${pathId}-${nodeId}`); }; return { - completedNodes, - loading, + completedNodes: actualCompletedNodes, + loading: actualLoading, toggleNode, isNodeCompleted, }; From 2bd8146e4bcce75462eb0187fb7cdd93e77ff4b3 Mon Sep 17 00:00:00 2001 From: Nitesh Agarwal Date: Sat, 13 Jun 2026 22:21:22 +0530 Subject: [PATCH 2/2] style: apply prettier formatting --- src/hooks/useLearningProgress.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/useLearningProgress.ts b/src/hooks/useLearningProgress.ts index fcae2b63..aeb4a280 100644 --- a/src/hooks/useLearningProgress.ts +++ b/src/hooks/useLearningProgress.ts @@ -1,4 +1,4 @@ -"use client"; +'use client'; import { useState, useEffect } from 'react'; import { useAuth } from '@/context/AuthContext'; @@ -35,7 +35,7 @@ export function useLearningProgress(): UseLearningProgressResult { setLoading(false); }, (error) => { - console.error("Error listening to learning progress:", error); + console.error('Error listening to learning progress:', error); setLoading(false); } ); @@ -72,7 +72,7 @@ export function useLearningProgress(): UseLearningProgressResult { { merge: true } ); } catch (error) { - console.error("Failed to save learning progress:", error); + console.error('Failed to save learning progress:', error); } };