diff --git a/components/chunk-load-error-handler.tsx b/components/chunk-load-error-handler.tsx index a072da1b..19780faf 100644 --- a/components/chunk-load-error-handler.tsx +++ b/components/chunk-load-error-handler.tsx @@ -11,19 +11,58 @@ export default function ChunkLoadErrorHandler() { useEffect(() => { // Key to track if we've already refreshed for this error const REFRESH_KEY = "chunk-load-error-refreshed"; + const REFRESH_TIMESTAMP_KEY = "chunk-load-error-timestamp"; + // Flag to prevent multiple simultaneous refresh attempts + let isRefreshing = false; + + /** + * Check if error is actually a ChunkLoadError + * This is more specific to avoid false positives + */ + const isChunkLoadError = (error: any, message?: string): boolean => { + // Direct ChunkLoadError check + if (error?.name === "ChunkLoadError") return true; + + // Check for specific chunk loading patterns (for webpack/next.js) + const errorMessage = error?.message || message || ""; + const isChunkError = + errorMessage.includes("Loading chunk") || + errorMessage.includes("ChunkLoadError") || + (errorMessage.includes("Failed to fetch") && errorMessage.includes("_next/static")) || + (errorMessage.includes("dynamically imported module") && errorMessage.includes("_next")); + + // Exclude normal network errors or API errors + const isNotApiError = + !errorMessage.includes("/api/") && + !errorMessage.includes("http") && + !errorMessage.includes("https") && + !errorMessage.includes("service/") && + !errorMessage.includes("endpoint"); + + return isChunkError && isNotApiError; + }; + + /** + * Check if we should refresh based on time + * Prevent refresh if last refresh was less than 5 seconds ago + */ + const shouldRefresh = (): boolean => { + const lastRefreshTime = sessionStorage.getItem(REFRESH_TIMESTAMP_KEY); + if (!lastRefreshTime) return true; + + const timeSinceLastRefresh = Date.now() - parseInt(lastRefreshTime, 10); + return timeSinceLastRefresh > 5000; // 5 seconds + }; + // Handle unhandled promise rejections (for dynamic imports) const handlePromiseRejection = (event: PromiseRejectionEvent) => { const error = event.reason; - // Check if it's a ChunkLoadError - if ( - error?.name === "ChunkLoadError" || - error?.message?.includes("Loading chunk") || - error?.message?.includes("ChunkLoadError") || - error?.message?.includes("Failed to fetch dynamically imported module") || - error?.message?.includes("failed to load resource") - ) { + // Only handle if it's actually a ChunkLoadError + if (isChunkLoadError(error)) { + console.warn("ChunkLoadError detected in promise rejection:", error); + event.preventDefault(); // Prevent default error handling handleChunkLoadError(); } }; @@ -32,46 +71,72 @@ export default function ChunkLoadErrorHandler() { const handleError = (event: ErrorEvent) => { const error = event.error; - // Check if it's a ChunkLoadError - if ( - error?.name === "ChunkLoadError" || - event.message?.includes("Loading chunk") || - event.message?.includes("ChunkLoadError") || - event.message?.includes("Failed to fetch dynamically imported module") - ) { + // Only handle if it's actually a ChunkLoadError + if (isChunkLoadError(error, event.message)) { + console.warn("ChunkLoadError detected in error event:", error); + event.preventDefault(); // Prevent default error handling handleChunkLoadError(); } }; const handleChunkLoadError = () => { - // Check if we've already refreshed + // Prevent multiple simultaneous refresh attempts + if (isRefreshing) { + console.log("Refresh already in progress, skipping..."); + return; + } + + // Check if we've already refreshed recently const hasRefreshed = sessionStorage.getItem(REFRESH_KEY); - if (!hasRefreshed) { - console.log("ChunkLoadError detected. Refreshing page..."); + if (!hasRefreshed && shouldRefresh()) { + isRefreshing = true; + + console.log("ChunkLoadError confirmed. Refreshing page in 100ms..."); // Mark that we've refreshed sessionStorage.setItem(REFRESH_KEY, "true"); + sessionStorage.setItem(REFRESH_TIMESTAMP_KEY, Date.now().toString()); - // Force reload the page (bypassing cache) - window.location.reload(); - } else { + // Small delay to allow logging and prevent immediate re-trigger + setTimeout(() => { + // Force reload the page (bypassing cache) + window.location.reload(); + }, 100); + } else if (hasRefreshed) { console.error("ChunkLoadError persists after refresh. There might be a deeper issue."); + console.error("Please try clearing your browser cache or contact support."); - // Clear the flag after 30 seconds in case user navigates away and comes back + // Clear the flag after 1 minute in case user navigates away and comes back setTimeout(() => { sessionStorage.removeItem(REFRESH_KEY); - }, 30000); + sessionStorage.removeItem(REFRESH_TIMESTAMP_KEY); + }, 60000); } }; + // Clear old refresh flags on component mount (for new session) + const clearOldFlags = () => { + const lastRefreshTime = sessionStorage.getItem(REFRESH_TIMESTAMP_KEY); + if (lastRefreshTime) { + const timeSinceLastRefresh = Date.now() - parseInt(lastRefreshTime, 10); + // Clear if last refresh was more than 5 minutes ago + if (timeSinceLastRefresh > 300000) { + sessionStorage.removeItem(REFRESH_KEY); + sessionStorage.removeItem(REFRESH_TIMESTAMP_KEY); + } + } + }; + + clearOldFlags(); + // Add event listeners - window.addEventListener("error", handleError); + window.addEventListener("error", handleError, true); // Use capture phase window.addEventListener("unhandledrejection", handlePromiseRejection); // Cleanup event listeners return () => { - window.removeEventListener("error", handleError); + window.removeEventListener("error", handleError, true); window.removeEventListener("unhandledrejection", handlePromiseRejection); }; }, []); diff --git a/components/form/content/spit-convert-form.tsx b/components/form/content/spit-convert-form.tsx index 47bc9157..a174365c 100644 --- a/components/form/content/spit-convert-form.tsx +++ b/components/form/content/spit-convert-form.tsx @@ -211,7 +211,20 @@ export default function FormConvertSPIT() { const [isUserMabesApprover, setIsUserMabesApprover] = useState(false); useEffect(() => { - initializeComponent(); + let isMounted = true; + + const init = async () => { + if (isMounted) { + await initializeComponent(); + } + }; + + init(); + + // Cleanup function to prevent state updates after unmount + return () => { + isMounted = false; + }; }, []); useEffect(() => { @@ -232,8 +245,15 @@ export default function FormConvertSPIT() { loadCategories(), id ? loadDetail() : Promise.resolve(), ]); - } catch (error) { + } catch (error: any) { console.error("Failed to initialize component:", error); + + // Don't show error alert if it's an AbortError (user navigated away or page refreshed) + if (error?.name === 'AbortError' || error?.message?.includes('abort')) { + console.log("Component initialization aborted (user navigation or refresh)"); + return; + } + MySwal.fire({ title: "Error", text: "Failed to load data. Please try again.", diff --git a/docs/CHUNK_LOAD_ERROR_HANDLER.md b/docs/CHUNK_LOAD_ERROR_HANDLER.md index 041b045c..33dabdd7 100644 --- a/docs/CHUNK_LOAD_ERROR_HANDLER.md +++ b/docs/CHUNK_LOAD_ERROR_HANDLER.md @@ -38,19 +38,34 @@ User opens app → Deployment happens → User navigates → ChunkLoadError occu ↓ Handler detects error ↓ + Validate it's actual ChunkLoadError + (not API error or normal network error) + ↓ Check sessionStorage ↓ - No refresh yet? → Yes → Set flag & Reload + No refresh yet? → Yes → Set flag & Reload (100ms delay) ↓ Already refreshed? → Show error in console ``` +### Error Detection Logic +Handler hanya mendeteksi ChunkLoadError yang spesifik: +- Error name adalah "ChunkLoadError" +- Error message mengandung "Loading chunk" atau "ChunkLoadError" +- Error terkait `_next/static` atau `_next` (Next.js chunks) +- **Mengecualikan** error API, HTTP requests, atau service calls + +Ini mencegah false positive dari Promise rejection biasa. + ## Benefits ✅ User experience lebih baik - no more broken page ✅ Automatic recovery dari ChunkLoadError ✅ Aman - hanya refresh sekali, tidak ada infinite loop ✅ Bekerja di background tanpa mengganggu user ✅ Works untuk semua tipe lazy-loaded components +✅ Tidak konflik dengan Promise rejection biasa (API calls, etc) +✅ Prevention untuk multiple simultaneous refresh +✅ Time-based protection (tidak refresh jika < 5 detik dari refresh terakhir) ## Testing Untuk test handler ini: