UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

119 lines (112 loc) 4.29 kB
'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 }; }