Skip to content

Instantly share code, notes, and snippets.

@raphael-leger
Last active July 30, 2024 08:46
Show Gist options
  • Save raphael-leger/4d703dea6c845788ff9eb36142374bdb to your computer and use it in GitHub Desktop.
Save raphael-leger/4d703dea6c845788ff9eb36142374bdb to your computer and use it in GitHub Desktop.
React LazyWithRetry
import { lazy } from 'react';
const lazyWithRetry = (componentImport) =>
lazy(async () => {
const pageHasAlreadyBeenForceRefreshed = JSON.parse(
window.localStorage.getItem(
'page-has-been-force-refreshed'
) || 'false'
);
try {
const component = await componentImport();
window.localStorage.setItem(
'page-has-been-force-refreshed',
'false'
);
return component;
} catch (error) {
if (!pageHasAlreadyBeenForceRefreshed) {
// Assuming that the user is not on the latest version of the application.
// Let's refresh the page immediately.
window.localStorage.setItem(
'page-has-been-force-refreshed',
'true'
);
return window.location.reload();
}
// The page has already been reloaded
// Assuming that user is already using the latest version of the application.
// Let's let the application crash and raise the error.
throw error;
}
});
@Markus1607
Copy link

@raphael-leger any chance you can provide the typescript version of this script?

@lisethgira
Copy link

lisethgira commented Apr 28, 2023

hola me gustaría que me ayudaras con este error ya que estoy tratando de dar una solución al chunkloaderror pero en angular

@victorlitvinenko
Copy link

victorlitvinenko commented May 10, 2023

@raphael-leger any chance you can provide the typescript version of this script?

import { ComponentType, lazy as originalLazy } from 'react';

type ImportComponent = () => Promise<{ default: ComponentType }>;

export const lazy = (importComponent: ImportComponent) =>
  originalLazy((async () => {
    const isPageHasBeenForceRefreshed = JSON.parse(
      localStorage.getItem('page-has-been-force-refreshed') || 'false',
    );

    try {
      const component = await importComponent();

      localStorage.setItem('page-has-been-force-refreshed', 'false');

      return component;
    } catch (error) {
      if (!isPageHasBeenForceRefreshed) {
        localStorage.setItem('page-has-been-force-refreshed', 'true');
        return location.reload();
      }

      throw error;
    }
  }) as ImportComponent);

@Markus1607
Copy link

@victorlitvinenko awesome thanks

@fatso83
Copy link

fatso83 commented May 12, 2023

AFAIK, the reloading approach only really works/makes sense in non-chromium browsers? From my own testing, failed module loading is persistent across manual reloads and browser restarts in chromium-derivates (Chrome, Edge, ...), and I was unable to get it to work again before manually clearing the cache.

So that's why we ended up going for the cache busting approach, as mentioned in Alon Mizrahi's article, when encountering this situation. I made a tiny library based on his approach (which parses the error message to get the module path), but improved on the cross-browser situation by finding another approach that works in Firefox (and probably all other Ecmascript engines as well). If you npm install @fatso83/retry-dynamic-import you can then use reactLazyWithRetry( () => import('./foo') ) or opt for wrapping the vanilla dynamicImportWithRetry in some other way.

@fatso83
Copy link

fatso83 commented May 12, 2023

@lisethgira Google Translate to the rescue 😄

I am not 100% sure how you do lazy loading of components in Angular, but you should essentially be able to just replace your import('./foo') with dynamicImportWithRetry( () => import('./foo'). I had a look at the Angular docs, which mention using loadChildren, so it would look something like this:

import {dynamicImportWithRetry} from '@fatso83/retry-dynamic-import';
//... other code

const routes: Routes = [
  {
    path: 'items',
    loadChildren: () => dynamicImportWithRetry(() => import('./items/items.module')).then(m => m.ItemsModule)
  }
];

@chrisbruford
Copy link

I was hitting a number of issues with the above code snippets - including:

  • Incompatible types
  • Lack of code completion on the resulting component
  • Using multiple lazyRetry functions on the same page would result in infinite loops caused by a successful component resetting the boolean for an unsuccessful component

Here's my result, incase it's useful to anyone else:

const lazyRetry = <T extends ComponentType<never>>(
  importComponent: () => Promise<{ default: T }>
): LazyExoticComponent<T> =>
  originalLazy<T>(async () => {
    const stringifiedFunction = importComponent.toString();
    const componentsRefreshed: string[] =
      JSON.parse(sessionStorage.getItem('reloaded-components')) || [];
    let refreshedComponents: Set<string>;

    if (Array.isArray(componentsRefreshed)) {
      refreshedComponents = new Set(componentsRefreshed);
    } else {
      throw Error('Unexpected value from data store');
    }

    const hasComponentRefreshed = refreshedComponents.has(stringifiedFunction);

    try {
      const component = await importComponent();
      refreshedComponents.delete(stringifiedFunction);
      sessionStorage.setItem(
        'reloaded-components',
        JSON.stringify(Array.from(refreshedComponents))
      );
      return component;
    } catch (error) {
      if (!hasComponentRefreshed) {
        refreshedComponents.add(stringifiedFunction);
        sessionStorage.setItem(
          'reloaded-components',
          JSON.stringify(Array.from(refreshedComponents))
        );
        location.reload();
      } else {
        throw error;
      }
    }
  });

export { lazyRetry };

@fatso83
Copy link

fatso83 commented Jul 21, 2023

@chrisbruford see my above comment regarding inapplicability in chromium browsers; does the forced reloading work for you? For me, the failed resolution seems persistent, and only cache busting the module will actually fix it (clearing cache also works).

From reading on the Chromium issue tracker, this is by design btw.

@chrisbruford
Copy link

@fatso83 we have some separate cache issues so I can't confirm just yet. Once they're resolved I'll be able to see if this turns up again and let you know.

@VitaliyKravetsWork
Copy link

@fatso83 we have some separate cache issues so I can't confirm just yet. Once they're resolved I'll be able to see if this turns up again and let you know.

It might be idiot question, but you don't use react laze so how it would work? and can you share import example

@fatso83
Copy link

fatso83 commented Nov 13, 2023

@VitaliyKravetsWork I am not totally sure what you are asking. I have shared code snippets showing how the library works. I suggest you check out the documentation for expanded code samples. There I show both the exports from @fatso83/retry-dynamic-import, as well as the utility wrapper in the sub-module @fatso83/retry-dynamic-import/react-lazy.

See if that makes sense, and if not, raise an issue in the issue tracker.

@VitaliyKravetsWork
Copy link

@VitaliyKravetsWork I am not totally sure what you are asking. I have shared code snippets showing how the library works. I suggest you check out the documentation for expanded code samples. There I show both the exports from @fatso83/retry-dynamic-import, as well as the utility wrapper in the sub-module @fatso83/retry-dynamic-import/react-lazy.

See if that makes sense, and if not, raise an issue in the issue tracker.

Thx for the response. I faced this issue. I tried this first code example from here and faced the infinite reload loop issue, Did I get you right your code example fix this infinite page refresh loop issue and work the same as the first code example from this topic?

@fatso83
Copy link

fatso83 commented Nov 14, 2023

I would think so. If not, submit an issue to the issue tracker

@Iam-cesar
Copy link

Iam-cesar commented Mar 15, 2024

This typescript version works fine for me in next:14

import { ComponentType } from "react";

const ATTEMPTS_LEFT = 2;

const isSessionStorageAvailable = typeof sessionStorage !== "undefined";

const setPageWasForcedToReload = () =>
  sessionStorage?.setItem(
    "page-has-been-forced-to-refresh-by-component-loader",
    "true",
  );

const setReloadedComponents = (refreshedComponents: Set<string>) =>
  sessionStorage?.setItem(
    "reloaded-components",
    JSON.stringify(Array.from(refreshedComponents)),
  );

const setComponentLoaderAttemptsLeft = (attempts: number) =>
  sessionStorage?.setItem("component-loader-attempts-left", `${attempts - 1}`);

const failedToAccessSessionStorage = (error: Error) =>
  console.error("Failed to access localStorage", error);

const componentLoader = (
  lazyComponent: () => Promise<{ default: ComponentType<any> }>,
): Promise<{ default: ComponentType<any> }> => {
  const stringifiedFunction = lazyComponent?.toString();
  let reloadTimeoutId: NodeJS.Timeout | null = null;

  const componentsRefreshed: string[] =
    (isSessionStorageAvailable &&
      JSON.parse(sessionStorage?.getItem("reloaded-components"))) ||
    [];

  const sessionStorageAttemptsLeft: string =
    (isSessionStorageAvailable &&
      JSON.parse(sessionStorage?.getItem("component-loader-attempts-left"))) ||
    ATTEMPTS_LEFT;

  const pageHasAlreadyBeenForcedToRefresh =
    JSON.parse(
      isSessionStorageAvailable &&
        sessionStorage?.getItem(
          "page-has-been-forced-to-refresh-by-component-loader",
        ),
    ) || "false";

  let refreshedComponents: Set<string>;

  if (Array.isArray(componentsRefreshed))
    refreshedComponents = new Set(componentsRefreshed);
  else throw Error("Unexpected value from data store");

  const hasComponentRefreshed = refreshedComponents?.has(stringifiedFunction);
  const hasToResolveComponentLoaderPromise =
    hasComponentRefreshed || !pageHasAlreadyBeenForcedToRefresh;

  return new Promise((resolve, reject) => {
    lazyComponent()
      .then(resolve)
      .catch((error) => {
        if (hasToResolveComponentLoaderPromise) {
          resolve({ default: () => null });
          return;
        }

        if (+sessionStorageAttemptsLeft <= 1) {
          try {
            refreshedComponents?.add(stringifiedFunction);
            if (isSessionStorageAvailable) {
              setPageWasForcedToReload();
              setReloadedComponents(refreshedComponents);
            }
          } catch (error) {
            failedToAccessSessionStorage(error);
          }

          // capture log errors ..:sentry, slack, etc..
        }

        try {
          if (isSessionStorageAvailable)
            setComponentLoaderAttemptsLeft(+sessionStorageAttemptsLeft);
        } catch (error) {
          failedToAccessSessionStorage(error);
        }

        if (reloadTimeoutId !== null) clearTimeout(reloadTimeoutId);

        reloadTimeoutId = setTimeout(() => window?.location?.reload(), 500);
      });
  });
};

export default componentLoader;

@chrisbruford
Copy link

@Iam-cesar if you suffix your opening code-block backticks with the language, you would enable syntax highlighting which would make this code block much easier to read 😉

like this:

```javascript
const foo = 'bar'
```

const foo = 'bar'

@Iam-cesar
Copy link

Iam-cesar commented Mar 25, 2024

@Iam-cesar if you suffix your opening code-block backticks with the language, you would enable syntax highlighting which would make this code block much easier to read 😉

@chrisbruford thank you very much, I didn't know that

@franciscoMerono
Copy link

franciscoMerono commented Apr 16, 2024

This typescript version works fine for me in next:14

import { ComponentType } from "react";

const ATTEMPTS_LEFT = 2;

const isSessionStorageAvailable = typeof sessionStorage !== "undefined";

const setPageWasForcedToReload = () =>
  sessionStorage?.setItem(
    "page-has-been-forced-to-refresh-by-component-loader",
    "true",
  );

const setReloadedComponents = (refreshedComponents: Set<string>) =>
  sessionStorage?.setItem(
    "reloaded-components",
    JSON.stringify(Array.from(refreshedComponents)),
  );

const setComponentLoaderAttemptsLeft = (attempts: number) =>
  sessionStorage?.setItem("component-loader-attempts-left", `${attempts - 1}`);

const failedToAccessSessionStorage = (error: Error) =>
  console.error("Failed to access localStorage", error);

const componentLoader = (
  lazyComponent: () => Promise<{ default: ComponentType<any> }>,
): Promise<{ default: ComponentType<any> }> => {
  const stringifiedFunction = lazyComponent?.toString();
  let reloadTimeoutId: NodeJS.Timeout | null = null;

  const componentsRefreshed: string[] =
    (isSessionStorageAvailable &&
      JSON.parse(sessionStorage?.getItem("reloaded-components"))) ||
    [];

  const sessionStorageAttemptsLeft: string =
    (isSessionStorageAvailable &&
      JSON.parse(sessionStorage?.getItem("component-loader-attempts-left"))) ||
    ATTEMPTS_LEFT;

  const pageHasAlreadyBeenForcedToRefresh =
    JSON.parse(
      isSessionStorageAvailable &&
        sessionStorage?.getItem(
          "page-has-been-forced-to-refresh-by-component-loader",
        ),
    ) || "false";

  let refreshedComponents: Set<string>;

  if (Array.isArray(componentsRefreshed))
    refreshedComponents = new Set(componentsRefreshed);
  else throw Error("Unexpected value from data store");

  const hasComponentRefreshed = refreshedComponents?.has(stringifiedFunction);
  const hasToResolveComponentLoaderPromise =
    hasComponentRefreshed || !pageHasAlreadyBeenForcedToRefresh;

  return new Promise((resolve, reject) => {
    lazyComponent()
      .then(resolve)
      .catch((error) => {
        if (hasToResolveComponentLoaderPromise) {
          resolve({ default: () => null });
          return;
        }

        if (+sessionStorageAttemptsLeft <= 1) {
          try {
            refreshedComponents?.add(stringifiedFunction);
            if (isSessionStorageAvailable) {
              setPageWasForcedToReload();
              setReloadedComponents(refreshedComponents);
            }
          } catch (error) {
            failedToAccessSessionStorage(error);
          }

          // capture log errors ..:sentry, slack, etc..
        }

        try {
          if (isSessionStorageAvailable)
            setComponentLoaderAttemptsLeft(+sessionStorageAttemptsLeft);
        } catch (error) {
          failedToAccessSessionStorage(error);
        }

        if (reloadTimeoutId !== null) clearTimeout(reloadTimeoutId);

        reloadTimeoutId = setTimeout(() => window?.location?.reload(), 500);
      });
  });
};

export default componentLoader;

Thanks for the code! But I think there is a little error.

  const pageHasAlreadyBeenForcedToRefresh =
    JSON.parse(
      isSessionStorageAvailable &&
        sessionStorage?.getItem(
          "page-has-been-forced-to-refresh-by-component-loader",
        ),
    ) || "false";

In this part of code, if 'page-has-been-forced-to-refresh-by-component-loader' not exists you assign "false", and after that you use pageHasAlreadyBeenForcedToRefresh as condition, but !"false" is false.

Correction:

    const pageHasAlreadyBeenForcedToRefresh =
      JSON.parse(
        (isSessionStorageAvailable &&
          sessionStorage?.getItem(
            'page-has-been-forced-to-refresh-by-component-loader'
          ))
      ) || false

Then I think hasToResolveComponentLoaderPromise should be

const hasToResolveComponentLoaderPromise = hasComponentRefreshed || pageHasAlreadyBeenForcedToRefresh

@gao-sun
Copy link

gao-sun commented Jul 26, 2024

Note

For who needs a fully-tested TypeScript implementation for this, I created a package react-safe-lazy for out-of-the-box usage.

To install:

npm i react-safe-lazy

Then just replace React.lazy with safeLazy and you are good to go.

import { safeLazy } from 'react-safe-lazy';

const MyComponent = safeLazy(() => import('./MyComponent'));

You can also customize the retries and reloads:

import { createSafeLazy } from 'react-safe-lazy';

const safeLazy = createSafeLazy({
  importRetries: 2,
  forceReload: {
    maxRetries: 1,
  },
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment