Warning
This is an internal project, and is not intended for public use. No support or stability guarantees are provided.
CoordinatedLazy shows a loading placeholder until its content is ready, then swaps to the content — coordinated with every other block on the page so they settle as a single update instead of a staggered cascade. createCoordinatedLazy builds a self-loading component on top of it (a demo, a chart, a code frame): one deferred piece that loads its own data and swaps itself in. To stream a list of these in on the client, use useStream; to inject build-time data, use abstractCreateStream. It is the base of the Coordinated Streaming pattern.
The server pre-renders both the placeholder and the content; the client decides which to show and swaps when ready flips:
useCoordinatedFallback; the swap collects it and hands it to the content via useCoordinatedContent, so the full content reuses what the placeholder already fetched.ready is true.useStream controller), so a page-wide change coordinated with useCoordinated can wait for the initial swaps to land.Drive the swap yourself with the CoordinatedLazy component when you already have the data and a ready flag:
'use client';
import { CoordinatedLazy } from '@mui/internal-docs-infra/CoordinatedLazy';
function Block({ ready, data }) {
return <CoordinatedLazy ready={ready} fallback={<Loading />} content={<Content data={data} />} />;
}
A code block frame is the simplest instance of this swap: its plain-text fallback → highlighted HAST is exactly a loading→full transition.
createCoordinatedLazy(config) builds a component that loads one piece's data and swaps from ChunkLoading to ChunkContent for you — no manual ready wiring. The user's generic props T flow through to both; data (type P) is the loaded value (or the initial value while loading).
const ChartChunk = createCoordinatedLazy({
ChunkContent,
ChunkLoading,
source: { mode: 'data', load: async (options) => fetchPoints(options) },
});
The returned component is isomorphic: each render it routes by how the data loads, so one component covers build, server, and client loading and server or client rendering.
| Situation | What renders | Result |
|---|---|---|
preloaded/controlled data (build precompute) | ChunkContent directly | content is in the server HTML — no swap |
a server Loader/InitialLoader, or a config source | ChunkServerLoader under Suspense | the loader runs + renders on the server, streams in (needs an RSC render context) |
no config loader + a ChunkProvider (or forceClient) | the 'use client' swap | placeholder on the server, then loads + swaps on the client from the ChunkProvider |
So ChunkContent may be a server component when the data is preloaded or comes from a server loader, or a client component using hooks on the client-loading path — and the server-component routing is automatic, not a manual composition.
source (discriminated)source is a discriminated union on mode, so each loading strategy is strongly typed — no overloads, no runtime return-sniffing:
mode | Loads |
|---|---|
data | one value directly (optionally a quick initial first) |
urls | a list of chunk URLs, then each URL's data |
stream | pushes chunks into an array and yields — progressive reveal |
The source loaders run on the server: createCoordinatedLazy routes a config source to ChunkServerLoader (it never serializes the loader functions into a client component). A piece uses preloaded/precomputed data when it has it (props-context-layering); to load on the client, supply the source through a ChunkProvider instead (which lazily imports it).
A server Loader streams its content in under Suspense, with ChunkLoading as the coarse placeholder — so a low-res baseline lands in the SSR HTML and swaps to the detailed content once the server resolves it. (A data-mode source's quick initial() paints the same way, on the server.)
import * as React from 'react';
import { createCoordinatedLazy } from '@mui/internal-docs-infra/CoordinatedLazy';
import type { Point } from './lineParts';
import FullChart from './FullChart';
import { SimpleChart } from './SimpleChart';
// A single chunk: the low-res baseline (`ChunkLoading`) is server-rendered into the
// SSR HTML, then the detailed line streams in from the server `Loader` under
// Suspense and swaps in once it resolves. The loader runs on the server (a config
// `source` would too); the browser only hydrates the streamed markup. For
// client-side loading instead, wrap the chunk in a `ChunkProvider`.
const ChartChunk = createCoordinatedLazy<{}, Point[]>({
ChunkContent: FullChart,
ChunkLoading: SimpleChart,
Loader: () => import('./FullChart'),
});
export function InitialDetailedChart() {
return (
<ChartChunk />
);
}Keep
initial()pure and isomorphic. It runs during render, so it must return the same value on the server and the client. ReadingDate.now(),Math.random(), orwindowfrom it produces a hydration mismatch. The same applies to any user props you feed the content during loading.
When the full payload restates content the placeholder already has, ship it compressed and decode it against that content as a DEFLATE dictionary. Here the placeholder paints plain prose and hoists it as the dictionary; the full content is the same prose marked up with comment threads — compressed HAST whose text is already in the dictionary, so only the markup-and-comments delta crosses the fallback→content channel (the same trick the code highlighter uses):
The team gathers feedback before each release, then reviews every change together. Small fixes ship quickly, while larger proposals wait for a second opinion.
'use client';
import * as React from 'react';
import { CoordinatedLazy, useCoordinatedFallback } from '@mui/internal-docs-infra/CoordinatedLazy';
import { Replayable } from '@/components/Replayable/Replayable';
import { CommentLayer } from './CommentLayer';
import { DocumentView } from './DocumentView';
import { HOISTED, PROSE } from './documentData';
function Loading() {
// Paint the plain prose and hoist it as the decompression dictionary, along
// with the byte sizes the content reports. Rendering the same DocumentView the
// content uses keeps the footer height constant — only the prose transitions.
useCoordinatedFallback(HOISTED);
return <DocumentView hoisted={HOISTED}>{PROSE}</DocumentView>;
}
function CommentedDocumentView() {
const [ready, setReady] = React.useState(false);
React.useEffect(() => {
const id = setTimeout(() => setReady(true), 1400);
return () => clearTimeout(id);
}, []);
// `requireHoist` holds the swap until the fallback hoists the prose, so the
// content always has the dictionary it needs to decode the comments.
return (
<CoordinatedLazy ready={ready} requireHoist fallback={<Loading />} content={<CommentLayer />} />
);
}
export function CommentedDocument() {
return (
<Replayable>
<CommentedDocumentView />
</Replayable>
);
}Build a self-loading CoordinatedLazy component. The returned component
is isomorphic: per render it evaluates buildChunkRenderInputs and
routes via resolveChunkRender, so one component covers build, server,
and client loading, and server or client rendering:
ChunkContent directly, so
build-precomputed data lands in the server HTML. ChunkContent may be a
server OR client component here.ChunkServerLoader
under a Suspense boundary (server Loader/InitialLoader or a server-side
data-mode load), so content loads and renders on the server and streams
in. Requires a server (RSC) render context; supports server-component content.'use client'
CoordinatedLazyClient, which loads on the client and swaps the
fallback to content. ChunkContent here must be a client component.The client-mode branch hands the (function-bearing) config to a 'use client'
component, so a client-loaded chunk must render inside a client subtree — call
createCoordinatedLazy from a client module, or wrap it in a client provider
(e.g. abstractCreateStream’s ClientProvider). Server-loaded and
preloaded/precomputed chunks have no such constraint — they render entirely on
the server path.
The user’s generic props T flow through to both components; data (type
P) is the loaded value (or the initial value while loading). Use it
standalone for any deferred piece (a demo, a chart, a code frame); useStream
renders a streamed list of them.
| Property | Type | Description |
|---|---|---|
| ChunkContent | | The full content component. |
| ChunkLoading | | The loading placeholder. Defaults to a component that renders |
| isLoaded | | Whether the preloaded value suffices for the full content. |
| isInitial | | Whether the preloaded value suffices for the initial state. |
| source | | Data source (discriminated by |
| Loader | | Server component rendered (under Suspense) to produce the full content. Always dynamically imported, and only imported when the render decision routes to it — so it never reaches the client bundle. |
| InitialLoader | | Server component rendered (under Suspense) to produce the initial state. |
| swap | | Swap timing forwarded to |
| loaderOptions | | Default options passed to the source loaders. |
| contentManagesSwap | | The |
| revalidateOnIdle | | Opt into stale-while-revalidate: once the chunk has loaded, automatically
re-run the loader once on the first idle period (via |
React.ComponentType<ChunkComponentProps<{}, P, O>>Loads a single piece's data on the client — from a directly-passed source, a surrounding ChunkProvider, or preloaded data — handling the quick initial value while the full data-mode load resolves. The component createCoordinatedLazy builds uses it internally for its client path (with a ChunkProvider source, since a config source runs on the server); call it directly with your own source for a custom client renderer.
Load a single chunk’s data on the client (props-context-layering: when the
data already arrived via preloaded, no fetch happens). Handles the
controlled/preloaded short-circuit and a quick initial value shown while
the full data-mode load resolves.
Returns a refresh() that re-runs the loader with stale-while-revalidate, and
(opt-in via config.revalidateOnIdle) schedules one such refresh on the first
idle period after the chunk has loaded.
Used by the component createCoordinatedLazy produces; consumers can
also call it directly for a custom chunk renderer.
| Parameter | Type | Description |
|---|---|---|
| config | | |
| props | |
UseChunkResult| Key | Type | Required |
|---|---|---|
| data | | Yes |
| loading | | Yes |
| revalidating | | Yes |
| refresh | | Yes |
To stream a list of pieces in on the client and coordinate their swaps, use useStream.
The placeholder paints a cheap version and hoists it; the content reads it back and refines it. This is how "quick now, detailed later" works without re-fetching (and what the compressed demo above uses).
// In the placeholder:
function Loading() {
useCoordinatedFallback({ dictionary: 'HELLO' });
return <PlainText />;
}
// In the content:
function Content() {
const { dictionary } = useCoordinatedContent();
return <Rich dictionary={dictionary} />;
}
CoordinatedLazy fires a preload(hoisted) callback the moment the placeholder hoists, so a consumer can start importing heavy helpers the data implies — in parallel with loading the content. Pair it with the ChunkProvider's usePreload to dedup those imports across instances.
When a config has a server Loader/InitialLoader or a source, createCoordinatedLazy routes to ChunkServerLoader under a Suspense boundary automatically — so the loader runs and renders on the server (per-chunk Suspense) and streams in. ChunkServerLoader (and LazyContentServer, for a lazily-imported component) are plain async render functions with no Node-only imports, so they ship from this same entry and are simply inert on the client; you can also place either under your own Suspense boundary for a fully manual server path. Server rendering requires the component to render in a server (RSC) context.
LazyContent lazily imports a component (server or client) and renders it once its chunk has loaded, reporting readiness to the settle gate — so a heavy content component can be code-split without breaking the coordinated swap. While the chunk loads it shows a fallback (or, inside a swap, the coordinating fallback), so the placeholder keeps covering the load with no empty flash. The widget's code is fetched only when it mounts — load it and watch the skeleton hold until the chunk lands:
'use client';
import * as React from 'react';
import { CoordinatedLazy, LazyContent } from '@mui/internal-docs-infra/CoordinatedLazy';
import { Replayable } from '@/components/Replayable/Replayable';
// Matches the widget's footprint so revealing it doesn't shift the layout.
function Skeleton() {
return (
<div
style={{
width: 280,
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'column',
gap: 12,
padding: 16,
borderRadius: 8,
border: '1px solid #d0cdd7',
background: '#faf9fc',
}}
>
{/* Each row matches the widget's row height (20 / 44 / 18) so revealing it
doesn't shift the layout. */}
<div style={{ height: 20, display: 'flex', alignItems: 'center' }}>
<div style={{ width: '55%', height: 14, borderRadius: 4, background: '#e7e4ee' }} />
</div>
<div style={{ display: 'flex', gap: 8 }}>
{[0, 1, 2].map((index) => (
<div
key={index}
style={{
boxSizing: 'border-box',
width: 44,
height: 44,
borderRadius: 8,
background: '#e7e4ee',
}}
/>
))}
</div>
<div style={{ height: 18, display: 'flex', alignItems: 'center' }}>
<div style={{ width: '40%', height: 12, borderRadius: 4, background: '#e7e4ee' }} />
</div>
</div>
);
}
function LazyWidgetView() {
const [ready, setReady] = React.useState(false);
// The swap shows the skeleton until `ready`, then reveals its `content`. That
// content is a `LazyContent`, so its chunk is fetched only once the swap mounts
// it: it keeps the *same* skeleton up (the coordinating fallback, so no explicit
// `fallback` is needed) until the code lands, then reports readiness — and the
// swap settles as one coordinated step rather than flashing an empty box.
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, alignItems: 'flex-start' }}>
<button
type="button"
onClick={() => setReady(true)}
disabled={ready}
style={{
font: '13px sans-serif',
padding: '6px 12px',
borderRadius: 6,
cursor: ready ? 'default' : 'pointer',
border: '1px solid #7c3aed',
background: ready ? '#f3eefe' : '#7c3aed',
color: ready ? '#7c3aed' : '#fff',
}}
>
{ready ? 'Widget loaded' : 'Load the widget'}
</button>
<CoordinatedLazy
ready={ready}
fallback={<Skeleton />}
content={<LazyContent content={() => import('./HeavyWidget')} />}
/>
</div>
);
}
export function LazyWidget() {
return (
<Replayable label="Reset">
<LazyWidgetView />
</Replayable>
);
}Because the placeholder already hoisted its cheap data, the content can also decide not to load the heavy path at all — and then the code-split chunk is never even imported. A natural case is annotations that only exist on preview deployments: in production the content reuses the hoisted plaintext as-is (no compressed payload, no renderer chunk); on a preview it loads the comments through LazyContent. Toggle between the two:
The team gathers feedback before each release, then reviews every change together. Small fixes ship quickly, while larger proposals wait for a second opinion.
'use client';
import * as React from 'react';
import {
CoordinatedLazy,
LazyContent,
useCoordinatedContent,
useCoordinatedFallback,
} from '@mui/internal-docs-infra/CoordinatedLazy';
import { DemoButton } from '@/components/DemoButton/DemoButton';
import { DocumentView } from './DocumentView';
import { HOISTED, PROSE, type Hoisted } from './documentData';
function ProductionNote() {
return (
<div style={{ font: '13px monospace', color: '#3f8f3f' }}>
production · comments skipped — +0 B over the wire, renderer chunk not loaded
</div>
);
}
function Loading() {
// Always paint the plain prose and hoist it as the dictionary — the cheap layer
// both deployments share.
useCoordinatedFallback(HOISTED);
return (
<DocumentView hoisted={HOISTED} footer={<ProductionNote />}>
{PROSE}
</DocumentView>
);
}
function ProductionContent() {
// Reuse the hoisted plaintext — no compressed payload decoded, no comment
// renderer imported. The full content is the dictionary the fallback already had.
const hoisted = useCoordinatedContent() as Hoisted;
return (
<DocumentView hoisted={hoisted} footer={<ProductionNote />}>
{hoisted.dictionary}
</DocumentView>
);
}
function Toggle({ preview, onChange }: { preview: boolean; onChange: (next: boolean) => void }) {
const active = { background: '#7c3aed', color: '#fff' };
return (
<div style={{ display: 'flex', gap: 6 }}>
<DemoButton style={!preview ? active : undefined} onClick={() => onChange(false)}>
Production
</DemoButton>
<DemoButton style={preview ? active : undefined} onClick={() => onChange(true)}>
Preview
</DemoButton>
</div>
);
}
export function ConditionalComments() {
const [preview, setPreview] = React.useState(false);
// The fallback hoists the plaintext either way. On the preview path the content
// is a code-split `LazyContent` that imports the comment renderer and decodes the
// compressed comments; on production the content just reuses the hoisted
// plaintext — so neither the payload nor the renderer chunk is ever loaded.
const content = preview ? (
<LazyContent content={() => import('./CommentLayerChunk')} />
) : (
<ProductionContent />
);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<Toggle preview={preview} onChange={setPreview} />
<CoordinatedLazy ready requireHoist fallback={<Loading />} content={content} />
</div>
);
}Lazily import a component and render it once its chunk has loaded, reporting
readiness to the settle gate — so the page can coordinate the swap and a
StreamController can reflect it in loading.
The import runs in an effect (not React.lazy + Suspense) on purpose: the swap
that reveals this content mounts/unmounts the subtree around a pending import(),
and a Suspense boundary that comes and goes around a pending promise trips React’s
async-info-on-boundary tracking (“cleaning up async info that was not on the
parent Suspense boundary”). Loading in an effect avoids a Suspense boundary
entirely, and renders only the fallback during SSR (effects don’t run there).
The import factory is captured once (via lazy useState), so an inline
content={() => import('./X')} doesn’t restart the import every render. While
the module loads, the explicit fallback (or, if none, the coordinating swap’s
fallback from CoordinatedContentContext) is shown — so the same
placeholder keeps covering the load, with no empty flash.
| Prop | Type | Description |
|---|---|---|
| content | | Dynamic import of the component to render. |
| fallback | | Placeholder shown while the module loads. Defaults to |
| gate | | Additional settle gate to report readiness to once the component has loaded
and mounted (e.g. a |
| props | | Props forwarded to the imported component. |
CoordinatedLazy is a thin wrapper around useCoordinatedSwap. Use the hook directly when the swap must fold the hoisted data into its own ready computation (as the code highlighter does to decompress before swapping).
Show fallback until ready (and the page-coordinated swap conditions) are
met, then swap to content. The fallback is force-mounted once so its
useCoordinatedFallback hoist runs even when the content is precomputed; the
hoisted data is handed down to content via useCoordinatedContent.
Generalizes the state-driven fallback<->content swap from
CodeHighlighterClient. Advanced consumers that need to fold the hoisted data
into their own ready computation should use useCoordinatedSwap
directly instead of this component.
| Prop | Type | Description |
|---|---|---|
| awaitContent | | Hold the swap until the content reports it has loaded, mounting the content
behind the fallback so a code-split (e.g. |
| content | | Full content, shown after the swap. Pre-rendered on the server. |
| data | | Arbitrary parent->fallback data exposed to the fallback subtree. |
| defer | | Hold the swap while real async work is in flight even though |
| fallback | | Loading placeholder; force-mounted once so its hoist hook runs. |
| gate | | Settle gate to register this swap with. When omitted, the ambient gate from
a surrounding coordinator (e.g. the |
| holdGate | | Hold the settle gate open without re-showing the fallback (content stays rendered). See . |
| preload | | Speculative preload hook. See : fired with the hoisted data so the consumer can start dynamic imports of helpers in parallel with loading the full content. |
| ready | | Whether the content’s data is ready to display. |
| requireHoist | | Hold the swap until the fallback hoists at least once. |
| skipFallback | | Skip the fallback entirely. |
Called by a fallback (loading) component to optionally hoist data the full
content will need and to signal that it mounted. Returns the parent->fallback
data and whether this fallback’s CoordinatedLazy is nested inside an
outer, still-loading one. Generalizes useCodeFallback.
Pass a memoized hoistData map; each entry is hoisted up to the swap, where
it is folded into the consumer’s ready decision and handed down to the full
content via useCoordinatedContent.
| Parameter | Type | Description |
|---|---|---|
| hoistData | |
UseCoordinatedFallbackResult| Key | Type | Required |
|---|---|---|
| data | | No |
| isNested | | Yes |
Read the data the fallback hoisted, from inside the full content. Lets the
content use what the fallback fetched (e.g. a decompression dictionary)
without the consumer threading it through props. Returns an empty map when
rendered outside a CoordinatedLazy.
Record<string, unknown>The generalized fallback<->content swap state machine extracted from
CodeHighlighterClient. Decides whether to show the fallback or the content,
owns the force-mount-once behavior, collects data hoisted up from the
fallback, suppresses nested-fallback flicker, and registers with a settle
gate so the page can coordinate when every initial swap has landed.
showFallback is the generalization of isFallbackRendered:
hasFallback && !skipFallback && (
!ready || defer || isNested || !fallbackMounted || (requireHoist && !hasHoisted)
)| Property | Type | Description |
|---|---|---|
| ready | | Whether the content’s data is ready to display. |
| defer | | Hold the swap while real async work is still in flight even though |
| holdGate | | Hold the settle gate open WITHOUT re-showing the fallback — the content stays
rendered while the page-wide coordination waits. Unlike |
| hasFallback | | Whether a fallback element exists to show. |
| skipFallback | | Skip the fallback entirely. |
| requireHoist | | Additionally hold the swap until the fallback hoists at least once. |
| awaitContent | | Hold the swap until the content reports it has loaded. The consumer mounts
the content (e.g. a |
| gate | | Settle gate to register this swap with. When omitted, the ambient gate from
a surrounding coordinator (e.g. the |
| data | | Arbitrary parent->fallback data exposed via the fallback context. |
| preload | | Fired as soon as the fallback hoists data, with the hoisted map. Lets the
consumer kick off dynamic |
UseCoordinatedSwapResult| Key | Type | Required |
|---|---|---|
| showFallback | | Yes |
| fallbackContext | | Yes |
| hoisted | | Yes |
| loading | | Yes |
| contentReady | | Yes |
| reportContentReady | | Yes |
| hoist | | Yes |
Props accepted by the component returned from createCoordinatedLazy.
type ChunkComponentProps<T extends {} = {}, P = unknown, O = unknown> = {
/** Build-time/precomputed value for this chunk. */
preloaded?: P;
/** Authoritative/controlled value: render content directly, never the loaders. */
controlled?: boolean;
/**
* Force client-side rendering: ignore the server `Loader`/`InitialLoader` for
* this render so the decision routes to a content/client branch instead. Lets
* a consumer that *configures* server loaders statically opt out of them
* per-render (e.g. when no server loading functions are available, or the
* caller explicitly wants the client to drive). Has no effect once
* `isLoaded`/`controlled` already render the content.
*/
forceClient?: boolean;
/**
* Per-render override for the `isInitial` decision input (whether the initial
* paint is already in hand). Mirrors how `controlled` overrides `isLoaded`:
* lets a consumer whose initial-readiness depends on per-render context it
* cannot express as a pure `config.isInitial(preloaded)` predicate compute it
* in its own router and pass the result. Takes precedence over
* `config.isInitial`.
*/
isInitial?: boolean;
/**
* For the server render modes (`server-loader`/`server-initial`), block the
* server render on the loader instead of streaming a fallback: render the
* server loader *without* a Suspense boundary, so its content lands in the
* initial HTML (e.g. for no-JS / crawler SSR). When unset (the default) the
* loader streams under Suspense, showing `ChunkLoading` until it resolves.
*/
awaitServerLoad?: boolean;
/**
* Skip the initial-loader stage: ignore the `InitialLoader` / source-`initial`
* for this render so a not-yet-loaded chunk loads the full content directly
* rather than fetching a quick initial first. For consumers that have no
* loading UI to show an initial paint into (so a 2-stage initial->full load
* would be wasted).
*/
skipInitialLoad?: boolean;
/** Per-render loader options (merged over the config's `loaderOptions`). */
loaderOptions?: O;
/** User generic props forwarded to `ChunkContent` / `ChunkLoading`. */
userProps?: T;
/** Settle gate to register with (defaults to the surrounding controller / page). */
gate?: SettleGate;
}Props the full chunk content receives. Mirrors ContentProps<T>: the user’s
generic props T are merged in, plus the resolved data (of type P) and a
loading flag (false once the full data is in).
type ChunkContentProps<T extends {} = {}, P = unknown> = {
data?: P;
loading: boolean;
refresh?: () => Promise<void>;
revalidating?: boolean;
} & TProps the loading placeholder receives. Mirrors ContentLoadingProps<T>:
loading is always true, and data carries whatever initial/preloaded
value is available (of type P).
type ChunkLoadingProps<T extends {} = {}, P = unknown> = { data?: P; loading: true } & TResult of resolveChunkRender.
type ChunkRenderDecision = { mode: ChunkRenderMode; loading: boolean }Already-evaluated inputs to resolveChunkRender (decoupled from config shape).
type ChunkRenderInputs = {
/** Evaluated `isLoaded(preloaded)` (or the controlled override). */
isLoaded: boolean;
/** Evaluated `isInitial(preloaded)`. */
isInitial: boolean;
/** A server initial is configured: an `InitialLoader`, or a `data`-mode `source.initial`. */
hasServerInitial: boolean;
/** A server full loader is configured: a `Loader`, or a `data`-mode `source.load`. */
hasServerLoader: boolean;
}The branch of the render decision that applies for a chunk.
type ChunkRenderMode =
| 'content'
| 'content-initial'
| 'server-initial'
| 'server-loader'
| 'attempt-initial-client'Swap timing forwarded to the underlying CoordinatedLazy.
type ChunkSwapConfig = { defer?: boolean; requireHoist?: boolean; channelKey?: string | null }Carries the data the fallback hoisted down into the full content. A
CoordinatedLazy provides it around content after the swap; content reads
it via useCoordinatedContent.
type CoordinatedContentContext = React.Context<CoordinatedContentContextValue>The data the fallback hoisted, handed down to the full content so it can use
what the fallback fetched (e.g. a DEFLATE dictionary). Read via
useCoordinatedContent.
type CoordinatedContentContextValue = {
hoisted: Record<string, unknown>;
/**
* The content calls this once it has loaded (its dynamic import resolved), so
* the swap can register readiness with the settle gate. Used by `LazyContent`.
*/
reportReady?: () => void;
/**
* The loading fallback to show *while a dynamically-imported content loads*.
* After the swap reveals the content, a `LazyContent` shows this as its own
* Suspense fallback during the `import()` - so the same placeholder the swap
* showed keeps covering the load, with no empty flash. Generalizes "hand the
* `ContentLoading` to the lazy content".
*/
fallback?: React.ReactNode;
}Provided by a CoordinatedLazy to its fallback subtree while the fallback is
shown. Carries the upward hoist channel and the nested-suppression flag.
undefined outside a fallback subtree: a fallback reads it via
useCoordinatedFallback, and a nested CoordinatedLazy detects its presence
to know it is rendered inside an outer instance’s still-loading fallback.
type CoordinatedFallbackContext = React.Context<CoordinatedFallbackContextValue | undefined>Provided by a CoordinatedLazy to its fallback subtree while the
fallback is shown. Carries the upward hoist channel and the
nested-suppression flag. Generalizes CodeHighlighterFallbackContext
({ extraVariants, setFallbackHasts, onHookCalled }).
type CoordinatedFallbackContextValue = {
/**
* Hoist a keyed value up to the swap so it can be folded into the consumer's
* `ready` decision and handed down to the full content via
* [`CoordinatedContentContextValue`](#coordinatedcontentcontextvalue). Generalizes `setFallbackHasts`.
*/
hoist?: (key: string, value: unknown) => void;
/**
* Signal that the fallback's hoist hook ran. The generic swap force-mounts
* the fallback on its own, so this is optional - consumers (e.g.
* `CodeHighlighter`) use it to validate that the loading component wired its
* hoist hook. Generalizes `onHookCalled`.
*/
onReady?: () => void;
/**
* `true` when this instance is nested inside an outer `CoordinatedLazy` still
* showing its own fallback. The inner stays in fallback while set, collapsing
* a "fallback -> content -> fallback -> content" flicker into one transition.
* Generalizes `isNestedInsideOuterFallback`.
*/
isNested?: boolean;
/** Arbitrary parent->fallback data (generalizes `extraVariants`). */
data?: Record<string, unknown>;
}The ambient settle gate that a CoordinatedLazy swap registers with
when it isn’t given an explicit gate prop. A coordinator (e.g. the
useStream controller) provides its gate here so every swap rendered beneath
it reports into the same gate — that is how a group’s loading reflects each
piece’s swap without threading a gate prop through every one. null outside
any coordinator, in which case the swap registers only with the page-global
gate.
type CoordinatedGateContext = React.Context<SettleGate | null>Props for CoordinatedLazy.
type CoordinatedLazyProps = {
/** Full content, shown after the swap. Pre-rendered on the server. */
content: React.ReactNode;
/** Loading placeholder; force-mounted once so its hoist hook runs. */
fallback?: React.ReactNode;
/** Whether the content's data is ready to display. */
ready: boolean;
/** Hold the swap while real async work is in flight even though `ready`. */
defer?: boolean;
/**
* Hold the settle gate open without re-showing the fallback (content stays
* rendered). See .
*/
holdGate?: boolean;
/** Skip the fallback entirely. */
skipFallback?: boolean;
/** Hold the swap until the fallback hoists at least once. */
requireHoist?: boolean;
/**
* Hold the swap until the content reports it has loaded, mounting the content
* behind the fallback so a code-split (e.g. `LazyContent`) content can load in
* the background and reveal only once its chunk has arrived. See
* .
*/
awaitContent?: boolean;
/**
* Settle gate to register this swap with. When omitted, the ambient gate from
* a surrounding coordinator (e.g. the `useStream` controller, via
* [`CoordinatedGateContext`](#coordinatedgatecontext)) is used instead. The page-global gate is
* always registered on top of either, so a page-wide coordinated commit waits
* for the swap regardless.
*/
gate?: SettleGate;
/** Arbitrary parent->fallback data exposed to the fallback subtree. */
data?: Record<string, unknown>;
/**
* Speculative preload hook. See :
* fired with the hoisted data so the consumer can start dynamic imports of
* helpers in parallel with loading the full content.
*/
preload?: (hoisted: Record<string, unknown>) => void;
}Configuration for createCoordinatedLazy.
type CreateChunkConfig<T extends {} = {}, P = unknown, O = unknown> = {
/** The full content component. */
ChunkContent: React.ComponentType<ChunkContentProps<T, P>>;
/** The loading placeholder. Defaults to a component that renders `null`. */
ChunkLoading?: React.ComponentType<ChunkLoadingProps<T, P>>;
/** Whether the preloaded value suffices for the full content. */
isLoaded?: IsLoaded<P>;
/** Whether the preloaded value suffices for the initial state. */
isInitial?: IsInitial<P>;
/**
* Data source (discriminated by `mode`). Its loader functions run **on the
* server only** - a `data`-mode source is executed by `ChunkServerLoader`
* (`source.load` for the full content, `source.initial` for a quick streamed
* paint) and never serialized into a Client Component. To load on the *client*,
* supply the source through a `ChunkProvider` (which lazily imports it)
* rather than this field. (Calling `useChunk` directly inside your own client
* component with a `source` is still fine - no server/client boundary is
* crossed there.)
*/
source?: StreamSource<P, O>;
/**
* Server component rendered (under Suspense) to produce the full content.
* Always dynamically imported, and only imported when the render decision
* routes to it - so it never reaches the client bundle.
*/
Loader?: LazyComponentImport<ChunkContentProps<T, P>>;
/** Server component rendered (under Suspense) to produce the initial state. */
InitialLoader?: LazyComponentImport<ChunkContentProps<T, P>>;
/** Swap timing forwarded to `CoordinatedLazy`. */
swap?: ChunkSwapConfig;
/** Default options passed to the source loaders. */
loaderOptions?: O;
/**
* The `ChunkContent` component performs its own client-side loading and
* fallback->content swap. When set, the client-driven render modes render
* `ChunkContent` directly (with `loading: true`) instead of wrapping it in the
* framework's [`useChunk`](#usechunk)+swap (`CoordinatedLazyClient`) - so a
* self-managing content (e.g. one already built on `useCoordinatedSwap`) is not
* double-swapped. Server and content/`content-initial` modes are unaffected.
*/
contentManagesSwap?: boolean;
/**
* Opt into stale-while-revalidate: once the chunk has loaded, automatically
* re-run the loader once on the first idle period (via `requestIdleCallback`)
* to refresh potentially-stale data in the background. Client-only. The chunk
* keeps showing its current data while the refresh is in flight.
*/
revalidateOnIdle?: boolean;
}Props for LazyContent / LazyContentServer.
type LazyContentProps<T extends {} = {}> = {
/** Dynamic import of the component to render. */
content: LazyComponentImport<T>;
/** Props forwarded to the imported component. */
props?: T;
/** Placeholder shown while the module loads. Defaults to `null`. */
fallback?: React.ReactNode;
/**
* Additional settle gate to report readiness to once the component has loaded
* and mounted (e.g. a `useStream` controller gate). The page-global gate is
* always registered too. Client path only - the server path streams via
* Suspense and has no client gate to report to.
*/
gate?: SettleGate;
}Where a chunk’s data comes from — a discriminated union on mode, so
each strategy is strongly typed with no overloads or runtime return-type
sniffing:
'data' — load the chunk’s data directly (optionally with a quick
initial value first).'urls' — split into per-chunk URLs (loadUrls), then load each URL’s
data (loadChunk); supports an initial pass.'stream' — push chunks into the passed array over time and yield after
each, for progressive reveal (the generator’s return is the last-chunk
signal).type StreamSource<P = unknown, O = unknown> =
| {
mode: 'data';
load: (options: O, signal: AbortSignal) => Promise<P>;
initial?: (options: O) => P;
}
| {
mode: 'urls';
loadUrls: (options: O, signal: AbortSignal) => Promise<StreamUrlsResult>;
loadChunk: (url: URL, options: O, signal: AbortSignal) => Promise<P>;
initialUrls?: (options: O) => StreamUrlsResult;
initialChunk?: (url: URL, options: O) => P;
}
| {
mode: 'stream';
stream: (chunks: P[], options: O, signal: AbortSignal) => AsyncGenerator<void, void, void>;
}Result of a urls-mode loader: the chunk URLs to load individually, rather
than the data itself. lastChunk marks the final URL for last-chunk
completion when the total isn’t known up front.
type StreamUrlsResult = { chunks: URL[]; lastChunk?: boolean }Result of useChunk.
type UseChunkResult<P> = {
/** The chunk's data: the loaded value, or the initial/preloaded value while loading. */
data: P | undefined;
/** `true` until the full data has loaded. */
loading: boolean;
/** `true` while a background refresh is in flight; the current `data` stays visible. */
revalidating: boolean;
/**
* Re-run the `data`-mode loader and swap in fresh data, keeping the current
* data visible meanwhile (stale-while-revalidate). Aborts any prior in-flight
* refresh. A no-op for non-`data` sources or when no source resolves.
*/
refresh: () => Promise<void>;
}Result of useCoordinatedFallback.
type UseCoordinatedFallbackResult = {
/** Parent->fallback data provided via . */
data?: Record<string, unknown>;
/** Whether this fallback's `CoordinatedLazy` is nested inside an outer, still-loading one. */
isNested: boolean;
}Options for useCoordinatedSwap.
type UseCoordinatedSwapOptions = {
/** Whether the content's data is ready to display. */
ready: boolean;
/** Hold the swap while real async work is still in flight even though `ready`. */
defer?: boolean;
/**
* Hold the settle gate open WITHOUT re-showing the fallback - the content stays
* rendered while the page-wide coordination waits. Unlike `defer` (which holds
* the rendered fallback), this only affects gate registration, for content that
* has swapped in but is still finishing deferred work it gates internally (e.g.
* the code highlighter rendering plain text, then highlighting in place).
*/
holdGate?: boolean;
/** Whether a fallback element exists to show. */
hasFallback: boolean;
/** Skip the fallback entirely. */
skipFallback?: boolean;
/** Additionally hold the swap until the fallback hoists at least once. */
requireHoist?: boolean;
/**
* Hold the swap until the content reports it has loaded. The consumer mounts
* the content (e.g. a `LazyContent`) while the fallback is shown so it can
* load in the background, returning `null` until ready and then calling the
* content context's `reportReady` - so a code-split content component loads
* behind the placeholder and reveals only once its chunk has arrived.
*/
awaitContent?: boolean;
/**
* Settle gate to register this swap with. When omitted, the ambient gate from
* a surrounding coordinator (e.g. the `useStream` controller, via
* [`CoordinatedGateContext`](#coordinatedgatecontext)) is used instead. The page-global gate is
* always registered on top of either, so a page-wide coordinated commit waits
* for the swap regardless.
*/
gate?: SettleGate;
/** Arbitrary parent->fallback data exposed via the fallback context. */
data?: Record<string, unknown>;
/**
* Fired as soon as the fallback hoists data, with the hoisted map. Lets the
* consumer kick off dynamic `import()`s of heavy helpers it can tell from the
* data it will need - in parallel with loading the full content, instead of
* the content mounting and then requesting them in a serial roundtrip. Should
* be idempotent (the module cache dedups within a graph); cross-instance
* dedup is the layout provider's job.
*/
preload?: (hoisted: Record<string, unknown>) => void;
}Result of useCoordinatedSwap.
type UseCoordinatedSwapResult = {
/** Whether the fallback branch should mount this render. */
showFallback: boolean;
/** Context value to provide to the fallback subtree. */
fallbackContext: CoordinatedFallbackContextValue;
/** Data hoisted up from the fallback so far, keyed. */
hoisted: Record<string, unknown>;
/** `true` while the fallback is being shown. */
loading: boolean;
/** In `awaitContent` mode, whether the content has reported it loaded. */
contentReady: boolean;
/** Passed to the content (via the content context) so it can report it loaded. */
reportContentReady: () => void;
/**
* Hoist a keyed value up to the swap directly (the same channel the fallback's
* `useCoordinatedFallback` uses). Lets the consumer populate the hoisted map
* from outside the fallback subtree - e.g. a client-loaded data path that has
* no fallback mounted but still needs to feed the hoisted dictionary.
*/
hoist: (key: string, value: unknown) => void;
}createStream factory