This commit is contained in:
Sabda Yagra 2025-12-09 08:51:52 +07:00
commit fa5a3e8240
3 changed files with 128 additions and 28 deletions

View File

@ -11,19 +11,58 @@ export default function ChunkLoadErrorHandler() {
useEffect(() => { useEffect(() => {
// Key to track if we've already refreshed for this error // Key to track if we've already refreshed for this error
const REFRESH_KEY = "chunk-load-error-refreshed"; 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) // Handle unhandled promise rejections (for dynamic imports)
const handlePromiseRejection = (event: PromiseRejectionEvent) => { const handlePromiseRejection = (event: PromiseRejectionEvent) => {
const error = event.reason; const error = event.reason;
// Check if it's a ChunkLoadError // Only handle if it's actually a ChunkLoadError
if ( if (isChunkLoadError(error)) {
error?.name === "ChunkLoadError" || console.warn("ChunkLoadError detected in promise rejection:", error);
error?.message?.includes("Loading chunk") || event.preventDefault(); // Prevent default error handling
error?.message?.includes("ChunkLoadError") ||
error?.message?.includes("Failed to fetch dynamically imported module") ||
error?.message?.includes("failed to load resource")
) {
handleChunkLoadError(); handleChunkLoadError();
} }
}; };
@ -32,46 +71,72 @@ export default function ChunkLoadErrorHandler() {
const handleError = (event: ErrorEvent) => { const handleError = (event: ErrorEvent) => {
const error = event.error; const error = event.error;
// Check if it's a ChunkLoadError // Only handle if it's actually a ChunkLoadError
if ( if (isChunkLoadError(error, event.message)) {
error?.name === "ChunkLoadError" || console.warn("ChunkLoadError detected in error event:", error);
event.message?.includes("Loading chunk") || event.preventDefault(); // Prevent default error handling
event.message?.includes("ChunkLoadError") ||
event.message?.includes("Failed to fetch dynamically imported module")
) {
handleChunkLoadError(); handleChunkLoadError();
} }
}; };
const 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); const hasRefreshed = sessionStorage.getItem(REFRESH_KEY);
if (!hasRefreshed) { if (!hasRefreshed && shouldRefresh()) {
console.log("ChunkLoadError detected. Refreshing page..."); isRefreshing = true;
console.log("ChunkLoadError confirmed. Refreshing page in 100ms...");
// Mark that we've refreshed // Mark that we've refreshed
sessionStorage.setItem(REFRESH_KEY, "true"); sessionStorage.setItem(REFRESH_KEY, "true");
sessionStorage.setItem(REFRESH_TIMESTAMP_KEY, Date.now().toString());
// Force reload the page (bypassing cache) // Small delay to allow logging and prevent immediate re-trigger
window.location.reload(); setTimeout(() => {
} else { // 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("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(() => { setTimeout(() => {
sessionStorage.removeItem(REFRESH_KEY); 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 // Add event listeners
window.addEventListener("error", handleError); window.addEventListener("error", handleError, true); // Use capture phase
window.addEventListener("unhandledrejection", handlePromiseRejection); window.addEventListener("unhandledrejection", handlePromiseRejection);
// Cleanup event listeners // Cleanup event listeners
return () => { return () => {
window.removeEventListener("error", handleError); window.removeEventListener("error", handleError, true);
window.removeEventListener("unhandledrejection", handlePromiseRejection); window.removeEventListener("unhandledrejection", handlePromiseRejection);
}; };
}, []); }, []);

View File

@ -211,7 +211,20 @@ export default function FormConvertSPIT() {
const [isUserMabesApprover, setIsUserMabesApprover] = useState(false); const [isUserMabesApprover, setIsUserMabesApprover] = useState(false);
useEffect(() => { 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(() => { useEffect(() => {
@ -232,8 +245,15 @@ export default function FormConvertSPIT() {
loadCategories(), loadCategories(),
id ? loadDetail() : Promise.resolve(), id ? loadDetail() : Promise.resolve(),
]); ]);
} catch (error) { } catch (error: any) {
console.error("Failed to initialize component:", error); 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({ MySwal.fire({
title: "Error", title: "Error",
text: "Failed to load data. Please try again.", text: "Failed to load data. Please try again.",

View File

@ -38,19 +38,34 @@ User opens app → Deployment happens → User navigates → ChunkLoadError occu
Handler detects error Handler detects error
Validate it's actual ChunkLoadError
(not API error or normal network error)
Check sessionStorage Check sessionStorage
No refresh yet? → Yes → Set flag & Reload No refresh yet? → Yes → Set flag & Reload (100ms delay)
Already refreshed? → Show error in console 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 ## Benefits
✅ User experience lebih baik - no more broken page ✅ User experience lebih baik - no more broken page
✅ Automatic recovery dari ChunkLoadError ✅ Automatic recovery dari ChunkLoadError
✅ Aman - hanya refresh sekali, tidak ada infinite loop ✅ Aman - hanya refresh sekali, tidak ada infinite loop
✅ Bekerja di background tanpa mengganggu user ✅ Bekerja di background tanpa mengganggu user
✅ Works untuk semua tipe lazy-loaded components ✅ 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 ## Testing
Untuk test handler ini: Untuk test handler ini: