UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

135 lines (128 loc) 5.08 kB
'use client'; import * as React from 'react'; import { useChunkContext } from "../ChunkProvider/ChunkContext.mjs"; import { requestIdle } from "../useCoordinated/scheduleTasks.mjs"; /** Result of {@link useChunk}. */ /** * 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 {@link createCoordinatedLazy} produces; consumers can * also call it directly for a custom chunk renderer. */ export function useChunk(config, props = {}) { const { preloaded, controlled } = props; const options = props.loaderOptions ?? config.loaderOptions; const chunkContext = useChunkContext(); const isLoaded = Boolean(controlled) || (config.isLoaded ? config.isLoaded(preloaded) : preloaded !== undefined); // The value shown while loading: the preloaded value if present, otherwise a // quick `initial` computed by a `data`-mode source. const initialData = React.useMemo(() => { if (preloaded !== undefined) { return preloaded; } const source = config.source; if (source && source.mode === 'data' && source.initial) { return source.initial(options); } return undefined; }, [preloaded, config, options]); const [data, setData] = React.useState(isLoaded ? preloaded : initialData); // `true` once the async `data`-mode load has resolved. Derive `loading` during // render so that when `isLoaded` becomes true after mount (a `preloaded` value // or `controlled` flag arriving later via props) `loading` flips to false on // the next render without an extra effect pass. const [loaded, setLoaded] = React.useState(false); const [revalidating, setRevalidating] = React.useState(false); const loading = !isLoaded && !loaded; React.useEffect(() => { if (isLoaded) { return undefined; } const controller = new AbortController(); (async () => { try { // Prefer the config source; otherwise fall back to a `ChunkProvider`'s // lazily-imported source (props-context-layering). The provider only // imports the loader module here - never when the chunk is preloaded. let source = config.source; if (!source && chunkContext) { source = await chunkContext.resolveSource(); } // Only a `data`-mode source loads a single chunk on the client; `urls` / // `stream` sources are driven by `useStream` at the list level. if (!source || source.mode !== 'data') { return; } const result = await source.load(options, controller.signal); if (!controller.signal.aborted) { setData(result); setLoaded(true); } } catch { // Aborted by a newer load, or the load failed - leave the loading // state in place for the consumer's fallback / a retry. } })(); return () => controller.abort(); }, [isLoaded, config, options, chunkContext]); // A `refresh()` re-runs the `data`-mode loader, keeping the current data // visible (stale-while-revalidate). Serialized via a ref so a newer refresh // aborts an older one and the latest result wins. const refreshControllerRef = React.useRef(null); const refresh = React.useCallback(async () => { refreshControllerRef.current?.abort(); const controller = new AbortController(); refreshControllerRef.current = controller; let source = config.source; if (!source && chunkContext) { source = await chunkContext.resolveSource(); } if (!source || source.mode !== 'data' || controller.signal.aborted) { return; } setRevalidating(true); try { const result = await source.load(options, controller.signal); if (!controller.signal.aborted) { setData(result); setLoaded(true); setRevalidating(false); } } catch { // Aborted by a newer refresh, or the load failed - keep the current data. if (!controller.signal.aborted) { setRevalidating(false); } } }, [config, options, chunkContext]); // Opt-in stale-while-revalidate: once the chunk has loaded, revalidate in the // background on the first idle period. Browser-only; cancelled on unmount. React.useEffect(() => { if (!config.revalidateOnIdle || loading || typeof window === 'undefined') { return undefined; } return requestIdle(() => { refresh().catch(() => {}); }); }, [config.revalidateOnIdle, loading, refresh]); // Abort any in-flight refresh on unmount. React.useEffect(() => () => { refreshControllerRef.current?.abort(); }, []); return { data, loading, revalidating, refresh }; }