react-lazy-chunk-recover
v0.1.2
Published
A React.lazy-compatible loader that recovers from stale code-split chunks after deployments.
Maintainers
Readme
react-lazy-chunk-recover
A small, typed replacement for React.lazy that attempts one guarded reload when
a code-split module fails to load. Applications with a current asset manifest can
optionally supply an alternate import before the reload.
Open tab uses admin-oldHash.js
|
New deploy removes oldHash.js
|
Route navigation -> dynamic import fails -> one guarded page reload -> current index.htmlInstall
npm install react-lazy-chunk-recoverreact is a peer dependency. The package supports React >=16.8; development
and CI target Node 18+ and current React.
Basic usage
It replaces lazy one-for-one.
- import { lazy } from 'react';
+ import { lazyWithRetry } from 'react-lazy-chunk-recover';
- const Admin = lazy(() => import('./Admin'));
+ const Admin = lazyWithRetry(() => import('./Admin'));Use it with your existing Suspense boundary:
const Settings = lazyWithRetry(() => import('./Settings'));
<Suspense fallback={<Spinner />}>
<Settings />
</Suspense>Options
| Option | Default | Purpose |
| --- | --- | --- |
| isRecoverableError | Built-in cross-browser matcher | Identifies stale chunk errors. |
| onStaleChunk | window.location.reload() | Custom recovery callback. The original error is then rethrown. |
| retries | 0 | In-place attempts before recovery. |
| retryDelayMs | 300 | First retry delay in ms; subsequent delays double. |
| storageKey | __lazy_retry_reload__ | sessionStorage key for the reload guard. |
| moduleKey | Shared guard | Stable component identifier. Isolates guards so a different lazy component cannot clear this component's recovery attempt. |
| getFreshModule | None | Optional alternate loader called once after retries. Return a { default: Component } module from a trusted current asset URL. |
const Reports = lazyWithRetry(() => import('./Reports'), {
retries: 2,
retryDelayMs: 250,
onStaleChunk(error) {
console.warn('A deployment changed this route chunk.', error);
window.location.assign('/');
},
});For apps with multiple lazy routes, give each route a stable moduleKey:
const Admin = lazyWithRetry(() => import('./Admin'), { moduleKey: 'Admin' });
const Reports = lazyWithRetry(() => import('./Reports'), { moduleKey: 'Reports' });An app that publishes a fresh asset manifest can supply an alternate import.
The package cannot discover a new hashed chunk URL from a failed import() by
itself; your app must provide the URL and ensure it is trusted and compatible
with the running app:
const Admin = lazyWithRetry(() => import('./Admin'), {
moduleKey: 'Admin',
getFreshModule: async () => {
const response = await fetch('/asset-manifest.json', { cache: 'no-store' });
if (!response.ok) throw new Error('Could not load the current asset manifest');
const manifest = (await response.json()) as { admin: string };
// Only import URLs produced by your own trusted manifest.
return import(/* @vite-ignore */ manifest.admin);
},
});This advanced path depends on the bundler and deployment layout. Keep old
hashed assets available during rollout when possible. For Vite, consider its
vite:preloadError event
for failures outside React.lazy and serve HTML with Cache-Control: no-cache.
React Router v6/v7
Use a normal lazy component as the route element; Router and the bundler do not need special integration.
import { Route, Routes } from 'react-router-dom';
import { lazyWithRetry } from 'react-lazy-chunk-recover';
const Admin = lazyWithRetry(() => import('./routes/Admin'));
export function AppRoutes() {
return (
<Suspense fallback={<div>Loading…</div>}>
<Routes>
<Route path="/admin" element={<Admin />} />
</Routes>
</Suspense>
);
}For React Router's lazy route-module API, keep Router's own loader but wrap
individual dynamically imported UI components with lazyWithRetry when they
are rendered. The package supports dynamic-import error messages emitted by
Vite, Webpack, Rspack, and modern Safari/Firefox/Chromium browsers.
Combine with an Error Boundary
The first recoverable failure reloads the page. A repeated failure after that reload is deliberately rethrown so an Error Boundary can show a useful fallback instead of the application looping forever.
<AppErrorBoundary fallback={<p>Please refresh or try again later.</p>}>
<Suspense fallback={<Spinner />}>
<Admin />
</Suspense>
</AppErrorBoundary>How it works
When a dynamic import fails with a recognized load-error message, the loader
first makes the configured in-place retries. If supplied, getFreshModule
gets one chance to load a different module URL. It then writes a marker to
sessionStorage and reloads. If another load error happens while the marker
remains, the import error propagates to React instead of triggering another
reload. Repeating the same import may still hit browser caching; retries are
most useful for temporary network failures, not missing hashed assets.
A successful load clears the marker for its moduleKey (or the shared default
key), allowing a later deployment to recover normally. Browser access is
guarded, so during SSR the original import error is rethrown. If
sessionStorage is unavailable, automatic reload is disabled to avoid an
unbounded reload loop; the error reaches your Error Boundary.
