From ff2bf474ee893f9850d7034c794e9fa1b2ce754b Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Wed, 24 Sep 2025 05:00:42 +0800 Subject: [PATCH] FIX: Resolve rapid clicking and navigation issues in file manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed race conditions and loading problems when users click folders or navigation buttons too quickly. ## Problems Identified: ### 1. Race Conditions in Path Changes **Issue**: Fast clicking folders/back button caused multiple simultaneous requests - useEffect triggered on every currentPath change - No debouncing for path changes (only for manual refresh) - Multiple loadDirectory() calls executed concurrently - Later responses could overwrite earlier ones ### 2. Concurrent Request Conflicts **Issue**: loadDirectory() had basic isLoading check but insufficient protection - Multiple requests could run if timing was right - No tracking of which request was current - Stale responses could update UI incorrectly ### 3. Missing Request Cancellation **Issue**: No way to cancel outdated requests when user navigates rapidly - Old requests would complete and show wrong directory - Confusing UI state when mixed responses arrived ## Technical Solution: ### **Path Change Debouncing** ```typescript // Added 150ms debounce specifically for path changes const debouncedLoadDirectory = useCallback((path: string) => { if (pathChangeTimerRef.current) { clearTimeout(pathChangeTimerRef.current); } pathChangeTimerRef.current = setTimeout(() => { if (path !== lastPathChangeRef.current && sshSessionId) { loadDirectory(path); } }, 150); }, [sshSessionId, loadDirectory]); ``` ### **Request Race Condition Protection** ```typescript // Track current loading path for proper cancellation const currentLoadingPathRef = useRef(""); // Enhanced concurrent request prevention if (isLoading && currentLoadingPathRef.current !== path) { console.log("Directory loading already in progress, skipping:", path); return; } ``` ### **Stale Response Handling** ```typescript // Check if response is still relevant before updating UI if (currentLoadingPathRef.current !== path) { console.log("Directory load canceled, newer request in progress:", path); return; // Discard stale response } ``` ## Flow Improvements: **Before (Problematic):** 1. User clicks folder A → currentPath changes → useEffect → loadDirectory(A) 2. User quickly clicks folder B → currentPath changes → useEffect → loadDirectory(B) 3. Both requests run concurrently 4. Response A or B arrives randomly, wrong folder might show **After (Fixed):** 1. User clicks folder A → currentPath changes → debouncedLoadDirectory(A) 2. User quickly clicks folder B → currentPath changes → cancels A timer → debouncedLoadDirectory(B) 3. Only request B executes after 150ms 4. If A somehow runs, its response is discarded as stale ## User Experience: ✅ Rapid folder navigation works smoothly ✅ Back button rapid clicking handled properly ✅ No more loading wrong directories ✅ Proper loading states maintained ✅ No duplicate API requests ✅ Responsive feel with 150ms debounce (fast enough to feel instant) The file manager now handles rapid user interactions gracefully without race conditions or loading the wrong directory content. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../Apps/File Manager/FileManagerModern.tsx | 69 ++++++++++++++++--- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx index 24d62138..a804118a 100644 --- a/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx +++ b/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx @@ -153,6 +153,28 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) { // Track if initial directory load is done to prevent duplicate loading const initialLoadDoneRef = useRef(false); + // Track last path change to prevent rapid navigation issues + const lastPathChangeRef = useRef(""); + const pathChangeTimerRef = useRef(null); + // Track current loading request to handle cancellation + const currentLoadingPathRef = useRef(""); + + // Debounced directory loading for path changes + const debouncedLoadDirectory = useCallback((path: string) => { + // Clear any existing timer + if (pathChangeTimerRef.current) { + clearTimeout(pathChangeTimerRef.current); + } + + // Set new timer for debounced loading + pathChangeTimerRef.current = setTimeout(() => { + if (path !== lastPathChangeRef.current && sshSessionId) { + console.log("Loading directory after path change:", path); + lastPathChangeRef.current = path; + loadDirectory(path); + } + }, 150); // 150ms debounce for path changes + }, [sshSessionId, loadDirectory]); // File list update - only reload when path changes, not on initial connection useEffect(() => { @@ -160,11 +182,21 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) { // Skip the first load since it's handled in initializeSSHConnection if (!initialLoadDoneRef.current) { initialLoadDoneRef.current = true; + lastPathChangeRef.current = currentPath; return; } - handleRefreshDirectory(); + + // Use debounced loading for path changes to prevent rapid clicking issues + debouncedLoadDirectory(currentPath); } - }, [sshSessionId, currentPath]); + + // Cleanup timer on unmount or dependency change + return () => { + if (pathChangeTimerRef.current) { + clearTimeout(pathChangeTimerRef.current); + } + }; + }, [sshSessionId, currentPath, debouncedLoadDirectory]); // Handle file drag to external const handleFileDragStart = useCallback( @@ -261,6 +293,14 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) { return; } + // Prevent concurrent loading requests + if (isLoading && currentLoadingPathRef.current !== path) { + console.log("Directory loading already in progress, skipping:", path); + return; + } + + // Set current loading path for tracking + currentLoadingPathRef.current = path; setIsLoading(true); try { @@ -268,6 +308,12 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) { const response = await listSSHFiles(sshSessionId, path); + // Check if this is still the current request (avoid race conditions) + if (currentLoadingPathRef.current !== path) { + console.log("Directory load canceled, newer request in progress:", path); + return; + } + console.log("Directory response received:", response); const files = Array.isArray(response) ? response : response?.files || []; @@ -277,14 +323,21 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) { setFiles(files); clearSelection(); } catch (error: any) { - console.error("Failed to load directory:", error); - toast.error( - t("fileManager.failedToLoadDirectory") + ": " + (error.message || error) - ); + // Only show error if this is still the current request + if (currentLoadingPathRef.current === path) { + console.error("Failed to load directory:", error); + toast.error( + t("fileManager.failedToLoadDirectory") + ": " + (error.message || error) + ); + } } finally { - setIsLoading(false); + // Only clear loading if this is still the current request + if (currentLoadingPathRef.current === path) { + setIsLoading(false); + currentLoadingPathRef.current = ""; + } } - }, [sshSessionId, clearSelection, t]); + }, [sshSessionId, isLoading, clearSelection, t]); // Debounced refresh function - prevent excessive clicking const handleRefreshDirectory = useCallback(() => {