How to fix ChunkLoadError when loading new ReactJS app version

How to fix ChunkLoadError when loading new ReactJS app version

Do you or your users get a ChunkLoadError after your deploy updates? If you use code splitting and dynamic imports, old code can cause this issue.

1. What can cause the chunk load error?

One of the main reasons for a ChunkLoadError is that your chunk just doesn't exist anymore. And that can happen after every new deployment!

Let's say some of your chunks are dynamically named, like a number, or a contenthash. These names get created automatically by Webpack at build time.

Now imagine that your code is out in production, a user loads up your main app app.min.js. They are loving the experience (of course!), and now they want to dive deeper.

They click on a button to view their account settings. This is a separate chunk of code chunk-1a.min.js.

If there have been no changes since the user loaded the app, the chunk will load and they will carry on, blissfully enjoying your app.

But, what if between loading the app, and navigating to settings, you deployed a new version of the app. Maybe you just added a cool new feature so that users can add a profile picture.

And what if that had created a new chunk name for the account settings bundle chunk-1b.min.js. Chances are, your production environment will create a fresh build, that old chunk is gone, and the new one is ready for action.

But your user has already loaded the old version of your code, and it looks for the old chunk chunk-1a.min.js. Which is gone! ChunkLoadError!

2. How can I fix the chunk load error?

One option is to keep your old bundles available, but I don't really like that idea. You want your users to get the latest code and access those new features you've pushed!

And when you get that ChunkLoadError, it could be an indicator that the user's browser is trying to access old code. Because they have downloaded code that's now out of date, and it's looking for the old chunks.

A quick refresh by the user would fix this issue. But you can't expect them to know to do that. And we can do it for them (without them even knowing!).

And here's how you would do that in ReactJS.

If you are code-splitting in ReactJS, you are probably using React.lazy, and the code below will work with that function. But if you're code splitting with dynamic imports, you can apply the same logic (and put it in a catch).

Going back to our example of a separate chunk for the user settings, maybe you import that code like this.

const UserSettings = React.lazy(() => import(/* webpackChunkName: "userSettings" */ './settings')));

At the moment, we give React.lazy a function that imports our code for the chunk. We're going to change this to wrap the import inside another function called lazyRetry.

const UserSettings = React.lazy(() => lazyRetry(() => import(/* webpackChunkName: "userSettings" */ './settings')));

This lazyRetry function will handle refreshing the browser in the event of an error.

const lazyRetry = function(componentImport) {
    return new Promise((resolve, reject) => {
        // TO DO
    });
};

The new function takes the component import function as an argument (that used to be passed to React.lazy) and returns a Promise. It returns a Promise because we are passing this to React.lazy, and that's what React.lazy requires.

We need our new lazyRetry function to try to import the component and assuming everything goes well, resolve the Promise with the import.

When we refresh the browser, the code will get reloaded, so it won't know if this error is happening for the first time or not. We need a way to tell it. So to do that, we can store the information in sessionStorage.

// a function to retry loading a chunk to avoid chunk load error for out of date code
const lazyRetry = function(componentImport) {
    return new Promise((resolve, reject) => {
        // check if the window has already been refreshed
        const hasRefreshed = JSON.parse(
            window.sessionStorage.getItem('retry-lazy-refreshed') || 'false'
        );
        // try to import the component
        componentImport().then((component) => {
            window.sessionStorage.setItem('retry-lazy-refreshed', 'false'); // success so reset the refresh
            resolve(component);
        }).catch((error) => {
            if (!hasRefreshed) { // not been refreshed yet
                window.sessionStorage.setItem('retry-lazy-refreshed', 'true'); // we are now going to refresh
                return window.location.reload(); // refresh the page
            }
            reject(error); // Default error behaviour as already tried refresh
        });
    });
};

3. Multiple lazyRetry per route

The above code works fine if you split your code with route-based code splitting, which is what I tend to do.

In that situation, I would pass a value as a name to the lazyRetry function to identify the module.

const UserSettings = React.lazy(() => lazyRetry(() => import(/* webpackChunkName: "userSettings" */ './settings'), "userSettings"));

And I would include that name in the sessionStorage key so that the page only reloads once per import fail.

You only need to make these adjustments if you have multiple React.lazy imports per route. I have multiple bundles loading inside my React.lazy import, and the original lazyRetry handles any errors as intended, no matter which chunk fails to load.