Skip to content

Lazy loading ​

Components loaded on first use, and a prefetcher that loads them ahead of that once the app is idle.

tsx
import { lazyDialog, lazyNamed, onLazyOpenError, prefetcher } from '@adecore/ui';
tsx
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.

tsx
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.

tsx
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.

tsx
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.