Logo

dev-resources.site

for different kinds of informations.

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

Published at
8/2/2024
Categories
webdev
github
react
identity
Author
palomino
Categories
4 categories in total
webdev
open
github
open
react
open
identity
open
Author
8 person written this
palomino
open
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

identity Article's
30 articles in total
Favicon
Deploying and Configuring a Hybrid Identity Lab Using Bicep - Part 1: Active Directory Setup and Sync
Favicon
It’s cybersecurity’s kryptonite: Why are you still holding it?
Favicon
How to secure minimal api microservices with asp.net core identity
Favicon
How to verify NIN for Nigerians on the ecitizen platform.
Favicon
Simplified Configuration of SSO Profiles in AWS CLI Using SSO Sessions
Favicon
Google identity Platform
Favicon
Why Broken Links Are Costing You Brand Deals (And How to Fix It)
Favicon
How To Get There: Bridging The Technology Gap Preventing You From Adopting A Secrets-free Machine Identity Framework
Favicon
5 go-to-market lessons I learned from driving a developer-led growth product
Favicon
Revolutionizing Identity Resolution with Machine Learning: A Technical Overview
Favicon
Social Media Security: How to Protect Your Online Identity
Favicon
The Future of Web: How Web5 Transforms Identity and Data OwnerShip
Favicon
Private Self-Hosted OIDC AWS Authentication
Favicon
Opaque token vs JWT
Favicon
Implementing ASP.NET Identity for a Multi-Tenant Application: Best Practices
Favicon
Color palette in branding: How Logto generate a custom color scheme for your brand
Favicon
Concepts of a Ticket in ASP.NET Identity
Favicon
Understanding Single Sign-On (SSO) and SAML: Simplified
Favicon
When should I use JWTs?
Favicon
Bring your own sign-in UI to Logto Cloud
Favicon
Create a remark plugin to extract MDX reading time
Favicon
Everything you need to know about Base64
Favicon
How does the browser process the URL input in the address bar?
Favicon
Deep Linking AWS Console with all your AWS IAM Identity Center Roles
Favicon
Are You Prepared for the Next Cyber Attack? - IDArmor
Favicon
heaviside and Identity in PyTorch
Favicon
Is magic link sign-in dying? A closer look at its declining popularity
Favicon
Crafting Your Developer Identity: A Blueprint for 2024 🌟
Favicon
Use React.lazy with confidence: A safe way to load components when iterating fast
Favicon
Personal access tokens, machine-to-machine authentication, and API Keys definition and their real-world scenarios

Featured ones: