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();
});
};
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();
});
};
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 };
}
});
};
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;
}
});
};
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;
}
});
};
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.
Top comments (1)
How do you handle scenarios where multiple retries still fail due to persistent network issues? Great insights by the way!