@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
124 lines (116 loc) • 5.14 kB
JavaScript
'use client';
import * as React from 'react';
import { CoordinatedFallbackContext } from "./CoordinatedFallbackContext.mjs";
import { CoordinatedGateContext } from "./CoordinatedGateContext.mjs";
import { useSettleGate } from "../useCoordinated/useSettleGate.mjs";
import { pageSettleGate } from "../useCoordinated/pageSettleGate.mjs";
/**
* 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`:
* ```ts
* hasFallback && !skipFallback && (
* !ready || defer || isNested || !fallbackMounted || (requireHoist && !hasHoisted)
* )
* ```
*/
export function useCoordinatedSwap(options) {
const {
ready,
defer = false,
holdGate = false,
hasFallback,
skipFallback = false,
requireHoist = false,
awaitContent = false,
gate,
data
} = options;
// Nested inside an outer instance's still-loading fallback? Suppress our own
// swap so the page collapses to a single fallback->content transition.
const isNested = React.useContext(CoordinatedFallbackContext) !== undefined;
// The gate this swap registers with: the explicit `gate` option wins,
// otherwise the ambient gate a surrounding coordinator (e.g. the `useStream`
// controller) provided - so a group's `loading` reflects this swap without a
// `gate` prop threaded through.
const ambientGate = React.useContext(CoordinatedGateContext);
const effectiveGate = gate ?? ambientGate;
const [fallbackMounted, setFallbackMounted] = React.useState(false);
const [hoisted, setHoisted] = React.useState({});
const hasHoisted = Object.keys(hoisted).length > 0;
// In `awaitContent` mode the content is mounted behind the fallback and loads
// in the background (e.g. a `LazyContent` returning `null`); it calls
// `reportContentReady` once loaded so the swap can reveal it.
const [contentReady, setContentReady] = React.useState(false);
const reportContentReady = React.useCallback(() => setContentReady(true), []);
const showFallback = hasFallback && !skipFallback && (!ready || defer || isNested || !fallbackMounted || requireHoist && !hasHoisted || awaitContent && !contentReady);
// Force-mount-once: after the first commit in which a fallback exists, allow
// the swap. Owned here rather than driven by the fallback calling a hook, so
// any fallback works. A fallback's own hoist effect is a child effect, so it
// runs before this parent effect - hoisted data is in place by the time we
// flip `fallbackMounted` and the swap proceeds.
React.useEffect(() => {
if (!fallbackMounted && hasFallback && !skipFallback) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- force-mount-once latch: must flip AFTER the first fallback commit so child hoist effects run first; see comment above
setFallbackMounted(true);
}
}, [fallbackMounted, hasFallback, skipFallback]);
// Speculative preload: fire as soon as the fallback hoists data so the
// consumer can start dynamic imports of helpers it can tell it will need, in
// parallel with loading the full content. The callback is synced into a ref
// (the "latest ref" idiom) so an inline `preload` doesn't re-run the fire
// effect on identity change - its dep is the hoisted data, not the callback.
const preloadRef = React.useRef(options.preload);
React.useEffect(() => {
preloadRef.current = options.preload;
});
React.useEffect(() => {
if (hasHoisted) {
preloadRef.current?.(hoisted);
}
}, [hoisted, hasHoisted]);
const hoist = React.useCallback((key, value) => {
setHoisted(prev => {
if (Object.is(prev[key], value)) {
return prev;
}
return {
...prev,
[key]: value
};
});
}, []);
const onReady = React.useCallback(() => {
setFallbackMounted(true);
}, []);
const fallbackContext = React.useMemo(() => ({
hoist,
onReady,
isNested,
data
}), [hoist, onReady, isNested, data]);
// Register with the page-global gate so a page-wide coordinated commit waits
// for this swap, and additionally with an explicit (controller) gate when
// provided so a `StreamController`'s `loading` reflects this swap too. Both
// release once we've swapped (and aren't deferring); a no-fallback instance
// settles immediately.
// `holdGate` keeps the gate open while the content stays rendered (e.g. the
// code highlighter deferring its highlight pass in place), distinct from
// `defer` which holds the fallback.
const settled = !showFallback && !defer && !holdGate;
useSettleGate(settled, pageSettleGate);
useSettleGate(settled, effectiveGate ?? null);
return {
showFallback,
fallbackContext,
hoisted,
loading: showFallback,
contentReady,
reportContentReady,
hoist
};
}