DEV Community

Palomino for Logto

Posted on • Originally published at blog.logto.io

Use React.lazy with confidence: A safe way to load components when iterating fast

React.lazy is a great way to load components on demand and improve the performance of your app. However, sometimes it can lead to some issues like "ChunkLoadError" and "Loading chunk failed".


The dilemma

Nowadays, software development is moving faster under the popular "move fast and break things" philosophy. No judgment here - it's just the way things are. However, this fast pace can sometimes lead to issues, especially when it comes to loading components in React.

If you are working on a project that uses React.lazy to load components on demand, you might have encountered some issues like ChunkLoadError and Loading chunk failed. Here are some possible reasons:

  • There's a network issue, for example, the user's internet connection is slow or unstable.
  • The user is on an obsolete version of the app, and the browser is trying to load a chunk that doesn't exist anymore.

Usually, a simple refresh of the page can solve the problem, but it's not a great experience for the user. Imagine if a white screen appears when the user is navigating to another route - it's not a good look for your app.

Can we balance the need for speed with the need for a smooth user experience? Sure. Let me show you how (with TypeScript, of course).

The solution

A brute force solution can be to save all the versions of the chunks in the server, thus no more the "missing chunk" issue. As your app grows, this solution can become unfeasible due to increasing disk space requirements, and it still doesn't solve the network issue.

Given the fact that a retry or a refresh can solve the problem, we can implement these solutions in our code. Since the issue usually happens when the user is navigating to another route, we can solve it even without the user noticing. All we need to do is to build a wrapper around the React.lazy function that will handle the retries and the refreshes.

There are already some great articles on how to implement this kind of solution, so I'll focus on the idea and inner workings of the solution.

🌟 The final code is available in this GitHub repository and the react-safe-lazy package is available on NPM. The package is fully tested, extremely portable (minzipped ~700B), and ready to be used in your project.

Create the wrapper

The first step is to create a wrapper around the React.lazy function:

import { lazy, type ComponentType } from 'react';

// Use generic for the sake of a correct type inference
const safeLazy = <T>(importFunction: () => Promise<{ default: ComponentType<T> }>) => {
  return lazy(async () => {
    return await importFunction();
  });
};
Enter fullscreen mode Exit fullscreen mode

Handle the retries

For network issues, we can handle the retries by wrapping the importFunction in a tryImport function:

const safeLazy = <T>(importFunction: () => Promise<{ default: ComponentType<T> }>) => {
  let retries = 0;
  const tryImport = async () => {
    try {
      return await importFunction();
    } catch (error) {
      // Retry 3 times max
      if (retries < 3) {
        retries++;
        return tryImport();
      }
      throw error;
    }
  };

  return lazy(async () => {
    return await tryImport();
  });
};
Enter fullscreen mode Exit fullscreen mode

Looks simple, right? You can also implement the exponential backoff algorithm to handle the retries more efficiently.

Handle the refreshes

For the obsolete version issue, we can handle the refreshes by catching the error and refreshing the page:

const safeLazy = <T>(importFunction: () => Promise<{ default: ComponentType<T> }>) => {
  // ...tryImport function

  return lazy(async () => {
    try {
      return await tryImport();
    } catch (error) {
      window.location.reload();
      // Return a dummy component to match the return type of React.lazy
      return { default: () => null };
    }
  });
};
Enter fullscreen mode Exit fullscreen mode

However, this implementation is very dangerous, as it may cause an infinite loop of refreshes when the error cannot be solved by a refresh. Meanwhile, the app state will be lost during the refresh. So we need the help of sessionStorage to store the message that we've tried to refresh the page:

const safeLazy = <T>(importFunction: () => Promise<{ default: ComponentType<T> }>) => {
  // ...tryImport function

  return lazy(async () => {
    try {
      const component = await tryImport();

      // Clear the sessionStorage when the component is loaded successfully
      sessionStorage.removeItem('refreshed');

      return component;
    } catch (error) {
      if (!sessionStorage.getItem('refreshed')) {
        sessionStorage.setItem('refreshed', 'true');
        window.location.reload();
        return { default: () => null };
      }

      // Throw the error if the component cannot be loaded after a refresh
      throw error;
    }
  });
};
Enter fullscreen mode Exit fullscreen mode

Now, when we catch the error from the safeLazy function, we know it is something that cannot be solved by a refresh.

Multiple lazy components on the same page

There's still a hidden pitfall with the current implementation. If you have multiple lazy components on the same page, the infinite loop of refreshes can still happen because other components may reset the sessionStorage value. To solve this issue, we can use a unique key for each component:

const safeLazy = <T>(importFunction: () => Promise<{ default: ComponentType<T> }>) => {
  // ...tryImport function

  // The key can be anything unique for each component
  const storageKey = importFunction.toString();
  return lazy(async () => {
    try {
      const component = await tryImport();

      // Clear the sessionStorage when the component is loaded successfully
      sessionStorage.removeItem(storageKey);

      return component;
    } catch (error) {
      if (!sessionStorage.getItem(storageKey)) {
        sessionStorage.setItem(storageKey, 'true');
        window.location.reload();
        return { default: () => null };
      }

      // Throw the error if the component cannot be loaded after a refresh
      throw error;
    }
  });
};
Enter fullscreen mode Exit fullscreen mode

Now, each component will have its own sessionStorage key, and the infinite loop of refreshes will be avoided. We can continue to nitpick the solution, for example:

  • Gather all the keys in an array, thus only one storage key is needed.
  • Set a refresh limit to refresh the page more than one time before throwing an error.

But I think you get the idea. A comprehensive TypeScript solution with tests and configurations is available in the GitHub repository. I've also published the react-safe-lazy package on NPM, so you can use it in your project right away.

Conclusion

Software development is a delicate work, and even the smallest details can take effort to resolve. I hope this article can help you to gracefully handle the issues with React.lazy and improve the user experience of your app.

Try Logto Cloud for free

Top comments (1)

Collapse
 
gallowaydeveloper profile image
Galloway Developer

How do you handle scenarios where multiple retries still fail due to persistent network issues? Great insights by the way!