@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
119 lines (112 loc) • 4.29 kB
JavaScript
'use client';
import * as React from 'react';
import { streamChunks } from "./streamChunks.mjs";
import { useStreamController } from "./useStreamController.mjs";
import { requestIdle } from "../useCoordinated/scheduleTasks.mjs";
/** Options for {@link useStream}. */
/** Result of {@link useStream}. */
/**
* Stream a list of chunks on the client and own a `StreamController` that scopes
* their coordination. Render the returned `chunks` as chunk components inside
* the returned `Controller`; each chunk registers its swap with the controller,
* and the list's completion (`markLast`) plus those swaps drive `loading`.
*
* The controller runs in `streaming` mode, so it stays `loading` until the list
* finishes streaming - at which point the chunks present can settle it.
*
* `refresh()` (and the opt-in `revalidateOnIdle`) re-stream the list in the
* background and swap the result in atomically when it completes, without a
* loading flash — the current list stays visible the whole time.
*/
export function useStream(options) {
const {
source,
loaderOptions,
channelKey,
revalidateOnIdle
} = options;
const {
Controller,
loading: controllerLoading,
markLast
} = useStreamController({
streaming: true,
channelKey
});
const [chunks, setChunks] = React.useState([]);
const [streamComplete, setStreamComplete] = React.useState(false);
const [revalidating, setRevalidating] = React.useState(false);
const [refreshToken, setRefreshToken] = React.useState(0);
// Set by `refresh()` so the stream effect can tell a background revalidation
// (keep the current list, swap on complete) from an initial / source-change
// stream (reveal progressively). Read-and-cleared at the start of the effect.
const refreshRef = React.useRef(false);
const refresh = React.useCallback(() => {
refreshRef.current = true;
setRefreshToken(token => token + 1);
}, []);
React.useEffect(() => {
const controller = new AbortController();
const isRefresh = refreshRef.current;
refreshRef.current = false;
if (isRefresh) {
setRevalidating(true);
}
(async () => {
try {
const stream = streamChunks(source, loaderOptions, controller.signal);
// Snapshots accumulate and reveal in order as the source streams.
let latest = [];
for await (const snapshot of stream) {
if (controller.signal.aborted) {
return;
}
latest = snapshot.chunks;
// Initial / source-change: reveal progressively. A background refresh
// holds the current list and swaps once below (stale-while-revalidate).
if (!isRefresh) {
setChunks(snapshot.chunks);
if (snapshot.lastChunk) {
setStreamComplete(true);
markLast();
}
}
}
if (isRefresh && !controller.signal.aborted) {
setChunks(latest);
setRevalidating(false);
}
} catch {
// Stream aborted by a newer run, or the loader failed.
if (isRefresh && !controller.signal.aborted) {
setRevalidating(false);
}
}
})();
return () => controller.abort();
// Re-stream when the source identity changes or a refresh is requested;
// options are read once at stream start, and `markLast` is stable.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [source, refreshToken]);
// Opt-in stale-while-revalidate: once the list has finished streaming,
// revalidate in the background on the first idle period. Browser-only.
React.useEffect(() => {
if (!revalidateOnIdle || !streamComplete || typeof window === 'undefined') {
return undefined;
}
return requestIdle(() => refresh());
}, [revalidateOnIdle, streamComplete, refresh]);
// Loading until the list has finished streaming AND every rendered chunk has
// settled. The controller's `loading` only reflects chunk swaps (it settles
// immediately when no chunks have registered yet), so combine it with the
// list-streaming state.
const loading = !streamComplete || controllerLoading;
return {
chunks,
Controller,
loading,
streamComplete,
revalidating,
refresh
};
}