Last active
March 4, 2025 04:19
-
-
Save mberneti/28769391cf27f7580a55dedab342c63a to your computer and use it in GitHub Desktop.
This utility function retryDynamicImport enhances React’s lazy loading mechanism by adding retry logic with a versioned query parameter. It retries importing a component multiple times in case of failure, which can be useful for bypassing browser cache or dealing with intermittent network issues. It can be used as a drop-in replacement for React…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Usage: | |
// Replace React.lazy(() => import('x')); | |
// with retryDynamicImport(() => import('x')); | |
import { ComponentType, lazy } from 'react'; | |
const MAX_RETRY_COUNT = 15; | |
const RETRY_DELAY_MS = 500; | |
// Regex to extract the module URL from the import statement | |
const uriOrRelativePathRegex = /import\(["']([^)]+)['"]\)/; | |
// Function to extract the component URL from the dynamic import | |
const getRouteComponentUrl = (originalImport: () => Promise<any>): string | null => { | |
try { | |
const fnString = originalImport.toString(); | |
return fnString.match(uriOrRelativePathRegex)?.[1] || null; | |
} catch (e) { | |
console.error('Error extracting component URL:', e); | |
return null; | |
} | |
}; | |
// Function to create a retry import function with versioned query parameter | |
const getRetryImportFunction = ( | |
originalImport: () => Promise<any>, | |
retryCount: number | |
): (() => Promise<any>) => { | |
const importUrl = getRouteComponentUrl(originalImport); | |
if (!importUrl || retryCount === 0) { | |
return originalImport; | |
} | |
// Add or update the version query parameter in the import URL | |
const importUrlWithVersionQuery = importUrl.includes('?') | |
? `${importUrl}&v=${retryCount}-${Math.random().toString(36).substring(2)}` | |
: `${importUrl}?v=${retryCount}-${Math.random().toString(36).substring(2)}`; | |
return () => import(/* @vite-ignore */ importUrlWithVersionQuery); | |
}; | |
// Main function to wrap the dynamic import with retry logic | |
export function retryDynamicImport<T extends ComponentType<any>>( | |
importFunction: () => Promise<{ default: T }> | |
): React.LazyExoticComponent<T> { | |
let retryCount = 0; | |
const loadComponent = (): Promise<{ default: T }> => | |
new Promise((resolve, reject) => { | |
function tryLoadComponent() { | |
const retryImport = getRetryImportFunction(importFunction, retryCount); | |
retryImport() | |
.then((module) => { | |
if (retryCount > 0) { | |
console.log( | |
`Component loaded successfully after ${retryCount} ${retryCount === 1 ? 'retry' : 'retries'}.` | |
); | |
} | |
resolve(module); | |
}) | |
.catch((error) => { | |
retryCount += 1; | |
if (retryCount <= MAX_RETRY_COUNT) { | |
console.warn(`Retry attempt ${retryCount} failed, retrying...`); | |
setTimeout(() => { | |
tryLoadComponent(); | |
}, retryCount * RETRY_DELAY_MS); | |
} else { | |
console.error('Failed to load component after maximum retries. Reloading the page...'); | |
reject(error); | |
window.location.reload(); | |
} | |
}); | |
} | |
tryLoadComponent(); | |
}); | |
return lazy(() => loadComponent()); | |
} |
- Added exponential backoff to retry delay for more efficient retry attempts
- Made maxRetryCount and retryDelayMs configurable via function parameters
- Improved error handling and logging for better debugging and transparency
- Refactored code structure for flexibility and maintainability
import { ComponentType, lazy } from "react";
interface RetryOptions {
maxRetryCount?: number;
retryDelayMs?: number;
}
const DEFAULT_RETRY_OPTIONS: Required<RetryOptions> = {
maxRetryCount: 15,
retryDelayMs: 500,
};
const uriOrRelativePathRegex = /import\(["']([^)]+)['"]\)/;
const getRouteComponentUrl = (
originalImport: () => Promise<any>
): string | null => {
try {
const fnString = originalImport.toString();
return fnString.match(uriOrRelativePathRegex)?.[1] || null;
} catch (e) {
console.error("Error extracting component URL:", e);
return null;
}
};
const getRetryImportFunction = (
originalImport: () => Promise<any>,
retryCount: number
): (() => Promise<any>) => {
const importUrl = getRouteComponentUrl(originalImport);
if (!importUrl || retryCount === 0) {
return originalImport;
}
const importUrlWithVersionQuery = importUrl.includes("?")
? `${importUrl}&v=${retryCount}-${Math.random().toString(36).substring(2)}`
: `${importUrl}?v=${retryCount}-${Math.random().toString(36).substring(2)}`;
return () => import(/* @vite-ignore */ importUrlWithVersionQuery);
};
export function retryDynamicImport<T extends ComponentType<any>>(
importFunction: () => Promise<{ default: T }>,
options: RetryOptions = {}
): React.LazyExoticComponent<T> {
const { maxRetryCount, retryDelayMs } = {
...DEFAULT_RETRY_OPTIONS,
...options,
};
let retryCount = 0;
const loadComponent = (): Promise<{ default: T }> =>
new Promise((resolve, reject) => {
function tryLoadComponent() {
const retryImport = getRetryImportFunction(importFunction, retryCount);
retryImport()
.then((module) => {
if (retryCount > 0) {
console.log(
`Component loaded successfully after ${retryCount} ${
retryCount === 1 ? "retry" : "retries"
}.`
);
}
resolve(module);
})
.catch((error) => {
retryCount += 1;
if (retryCount <= maxRetryCount) {
const delay = retryDelayMs * Math.pow(2, retryCount - 1); // Exponential backoff
console.warn(
`Retry attempt ${retryCount} failed, retrying in ${delay}ms...`
);
setTimeout(() => tryLoadComponent(), delay);
} else {
console.error(
"Failed to load component after maximum retries. Reloading the page..."
);
reject(error);
window.location.reload();
}
});
}
tryLoadComponent();
});
return lazy(() => loadComponent());
}
Here is how you can handle that in vue!
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
const MAX_RETRY_COUNT = 15;
const RETRY_DELAY_MS = 500;
const AsyncComponent = defineAsyncComponent({
loader: () => import('./MyComponent.vue'),
onError(_, retry, fail, attempts) {
if (attempts > MAX_RETRY_COUNT) fail();
else setTimeout(retry, RETRY_DELAY_MS);
},
})
</script>
<template>
<AsyncComponent />
</template>
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Refactor and optimize retryDynamicImport function
import
{ ComponentType, lazy } from 'react';const MAX_RETRY_COUNT = 15;
const RETRY_DELAY_MS = 500;
type ImportFunction = () => Promise<{ default: T }>;
function getVersionedUrl(url: string, retryCount: number): string {
const versionQuery =
v=${retryCount}-${Math.random().toString(36).slice(2)}
;return url.includes('?') ?
${url}&${versionQuery}
:${url}?${versionQuery}
;}
export function retryDynamicImport<T extends ComponentType>(
importFunction: ImportFunction
): React.LazyExoticComponent {
return lazy(() => {
let retryCount = 0;
});
}