@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
135 lines (128 loc) • 5.08 kB
JavaScript
'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
};
}