Lazy loading
Components loaded on first use, and a prefetcher that loads them ahead of that once the app is idle.
import { lazyDialog, lazyNamed, onLazyOpenError, prefetcher } from '@adecore/ui';import { Suspense, useState } from 'react';
import { Button, lazyNamed, prefetcher } from '@adecore/ui';
// A module the app splits off, loaded on its first use.
const Chart = lazyNamed(() => import('../shared/lazy-chart.tsx'), 'LazyChart');
export default function LazyLoadingDemo() {
const [shown, setShown] = useState(false);
return (
<div className="flex flex-col items-center gap-3">
<div className="flex gap-2">
<Button variant="secondary" onClick={() => setShown(!shown)}>
{shown ? 'Hide the chart' : 'Show the chart'}
</Button>
<Button onClick={() => void prefetcher.prefetchEverything()}>Prefetch everything</Button>
</div>
{shown && (
<Suspense fallback={<p className="text-xs text-text-muted">Loading...</p>}>
<Chart bars={[4, 7, 3, 9, 6]} />
</Suspense>
)}
</div>
);
}lazyNamed
lazyNamed(load, name) is React.lazy for a module that exports its component by name, default included. It answers a component with the same props, which suspends until the module is there. Once the module is loaded it draws straight away. React.lazy suspends for a tick even on a module the bundler already has, which flashes the fallback and would mount a dialog already open.
const HistoryPanel = lazyNamed(() => import('./HistoryPanel'), 'HistoryPanel');lazyDialog
lazyDialog(load, name, useStore, isOpen) is for a dialog that keeps its own open state in a store. Before its module is here, opening it loads it. Once it is here, prefetched or opened, it stays mounted while closed, so even its first opening animates like every later one.
const SettingsWindow = lazyDialog(() => import('./SettingsWindow'), 'SettingsWindow', useAppStore, (state) => state.settingsOpen);The prefetcher
Every module loaded through lazyNamed or lazyDialog registers with prefetcher. Call prefetcher.prefetchEverything() once the app has drawn its first screen, and it loads every registered module ahead of its first use, one per idle moment and never two at once. A module registered afterwards, such as a lazy surface inside another, joins the end of the queue. prefetcher.prefetch(load) loads one module ahead of the rest.
The prefetcher does nothing while the browser asks to save data. A failed prefetch is quiet; the first real use tries again. prefetcher.busy is true while a prefetch waits on its chunk, so a failure an app hears of elsewhere, such as the bundler's event for a chunk that did not load, may be the prefetch's while it is true. It may as well be a person's open at the same moment, which onLazyOpenError tells apart.
Prefetcher is the class behind it, for a test or a second queue: new Prefetcher(whenIdle, allowed?). WhenIdle is a function that calls back once the main thread is free, and Loader is a function that loads a module.
A failed open
onLazyOpenError(listener) calls listener with the error when a module that lazyNamed or lazyDialog loads for a render fails to load, which is a load a person waits on. A failed prefetch never reaches it, not even one of the same module that fails at the same moment. It returns a function that stops listening.
React.lazy keeps a rejected load, so the surface that failed stays down until the page loads again. That makes this the place to reload once after a deploy removed the chunks of the build the page runs.
const stop = onLazyOpenError((error) => console.warn('A lazy surface failed to load', error));LoadedComponent
LoadedComponent is the cache both helpers share: a loader, the component once it is loaded (current), load() and subscribe(listener). You rarely need it directly.