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;
}
});
@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