Skip to content

Instantly share code, notes, and snippets.

@mberneti
Last active March 4, 2025 04:19
Show Gist options
  • Save mberneti/28769391cf27f7580a55dedab342c63a to your computer and use it in GitHub Desktop.
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…
// 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());
}
@mpayafar
Copy link

Refactor and optimize retryDynamicImport function

  • Simplified code structure for better readability
  • Implemented async/await for cleaner asynchronous logic
  • Added more precise TypeScript types for improved type safety
  • Removed redundant helper functions and integrated their logic
  • Enhanced error handling and logging
  • Optimized performance by using original import on first attempt
  • Increased flexibility to handle various import function structures
  • Maintained core functionality while improving overall implementation

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;

const tryImport = async (): Promise<{ default: T }> => {
  try {
    // Use the original import function for the first attempt
    if (retryCount === 0) {
      return await importFunction();
    }

    // For retries, modify the import URL
    const moduleUrl = importFunction.toString().match(/import\(["']([^)]+)['"]\)/)?.[1];
    if (!moduleUrl) {
      throw new Error('Unable to extract module URL');
    }

    const versionedUrl = getVersionedUrl(moduleUrl, retryCount);
    const module = await import(/* @vite-ignore */ versionedUrl);

    console.log(`Component loaded successfully after ${retryCount} ${retryCount === 1 ? 'retry' : 'retries'}.`);
    return module;
  } catch (error) {
    if (retryCount >= MAX_RETRY_COUNT) {
      console.error('Failed to load component after maximum retries. Reloading the page...');
      window.location.reload();
      throw error;
    }

    retryCount++;
    console.warn(`Retry attempt ${retryCount} failed, retrying...`);
    await new Promise(resolve => setTimeout(resolve, retryCount * RETRY_DELAY_MS));
    return tryImport();
  }
};

return tryImport();

});
}

@AliRazaviDeveloper
Copy link

AliRazaviDeveloper commented Oct 25, 2024

  • 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());
}

@biomousavi
Copy link

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