@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
469 lines (454 loc) • 20.7 kB
JavaScript
'use client';
import * as React from 'react';
import { registerPeer, reportValue, announceTarget, getBarrierAnnounceTime } from "./coordinatePreference.mjs";
import { whenLayoutShiftsSettled, layoutShiftsSettled } from "./layoutShiftGate.mjs";
/**
* Options for {@link useCoordinated}. See `coordinatePreference` for
* the underlying semantics; only React-specific behaviors are
* documented here.
*/
let nextAutoPeerId = 0;
function generatePeerId() {
nextAutoPeerId += 1;
return `peer-${nextAutoPeerId}`;
}
/**
* Coordinate a piece of state across sibling component instances on
* the same channel, so that visually disruptive value changes commit
* in a single layout pass rather than independently. Designed as a
* thin wrapper around any `useState`-shaped primitive (e.g.
* `useLocalStorageState`, `usePreference`, plain `useState`).
*
* **Originator flow** — calling the returned `setValue`:
* 1. `pendingValue` updates synchronously to the requested target
* 2. The coordinator runs `preload` (per phase rules) and waits for
* sibling peers (phase 1 only)
* 3. When the barrier resolves, the underlying `setValue` is called
* and this hook's visible `value` flips, so the swap is
* consistent with the optional `onCommit` side-effect
*
* **Receiver flow** — when the underlying `value` changes from outside
* (e.g. a storage event from another tab):
* 1. `pendingValue` updates to match
* 2. Coordination runs locally (this tab's peers run their own
* phase-1 barrier)
* 3. The visible `value` returned by this hook is held at the
* previous value until the barrier resolves, then flips
*
* Pass `channelKey: null` to disable coordination — the hook becomes
* a transparent pass-through of the underlying tuple.
*/
export function useCoordinated(underlying, options) {
const [underlyingValue, setUnderlyingValue] = underlying;
const {
channelKey,
peerId: explicitPeerId,
causesLayoutShift,
preload,
onCommit,
minWaitMs,
multiPeerExtraMinWaitMs,
lazyMinWaitMs,
gracePeriodMs,
ultimateTimeoutMs,
animateDuringPreload = false,
lazyCommitPriority = 'idle'
} = options;
// Stable peer id for the lifetime of the mounted component. Held in a ref so an
// auto-generated fallback stays stable across renders, an explicit `peerId` prop
// wins and resyncs on change, and an explicit id later removed keeps the last
// explicit value. The registration effect keys off [channelKey, peerId].
/* eslint-disable react-hooks/refs -- deliberate stable-id ref: lazy init + explicit-prop resync during render; the ref is identity memory, never read for rendering output */
const peerIdRef = React.useRef(null);
if (peerIdRef.current === null) {
peerIdRef.current = explicitPeerId ?? generatePeerId();
} else if (explicitPeerId !== undefined && explicitPeerId !== peerIdRef.current) {
peerIdRef.current = explicitPeerId;
}
const peerId = peerIdRef.current;
/* eslint-enable react-hooks/refs */
// The visible (committed) value. Lags `underlyingValue` while a
// phase-1 barrier is open in the receiver flow.
const [committedValue, setCommittedValue] = React.useState(underlyingValue);
// The latest target value (originator click or external change).
const [pendingValue, setPendingValue] = React.useState(underlyingValue);
const [isCoordinating, setIsCoordinating] = React.useState(false);
const [isWaitingForPeers, setIsWaitingForPeers] = React.useState(false);
// Keep latest callbacks in a ref so we don't re-register the peer
// when the consumer passes inline functions.
const callbacksRef = React.useRef({
causesLayoutShift,
preload,
onCommit,
setUnderlyingValue
});
/* eslint-disable react-hooks/refs -- latest-ref pattern: cache current callbacks for the once-registered peer and long-lived runCoordination closure; intentionally excluded from effect/useCallback deps to avoid re-registering on inline-function identity churn */
callbacksRef.current.causesLayoutShift = causesLayoutShift;
callbacksRef.current.preload = preload;
callbacksRef.current.onCommit = onCommit;
callbacksRef.current.setUnderlyingValue = setUnderlyingValue;
/* eslint-enable react-hooks/refs */
const timingRef = React.useRef({
minWaitMs,
multiPeerExtraMinWaitMs,
lazyMinWaitMs,
gracePeriodMs,
ultimateTimeoutMs,
animateDuringPreload,
lazyCommitPriority
});
/* eslint-disable react-hooks/refs -- latest-ref pattern: cache current timing options for the once-registered peer and long-lived runCoordination closure; intentionally excluded from effect/useCallback deps to avoid re-registering on option identity churn */
timingRef.current.minWaitMs = minWaitMs;
timingRef.current.multiPeerExtraMinWaitMs = multiPeerExtraMinWaitMs;
timingRef.current.lazyMinWaitMs = lazyMinWaitMs;
timingRef.current.gracePeriodMs = gracePeriodMs;
timingRef.current.ultimateTimeoutMs = ultimateTimeoutMs;
timingRef.current.animateDuringPreload = animateDuringPreload;
timingRef.current.lazyCommitPriority = lazyCommitPriority;
/* eslint-enable react-hooks/refs */
// In-flight handle so we can cancel/supersede.
const handleRef = React.useRef(null);
// Track the value we last asked the underlying primitive to take —
// when we see it echoed back via `underlyingValue` we skip the
// receiver flow so we don't double-coordinate our own writes.
const lastWrittenRef = React.useRef({
has: false
});
// The latest target the coordinator is working on; used to dedupe
// re-entrant effect runs when `underlyingValue` changes mid-flight.
const inFlightTargetRef = React.useRef({
has: false
});
// Forward-declared ref so the peer registration's `onSiblingAnnounce`
// can call the latest `runCoordination` without re-registering the
// peer every time the callback identity changes.
const runCoordinationRef = React.useRef(null);
// Register / unregister this peer with the channel.
React.useInsertionEffect(() => {
if (channelKey === null) {
return undefined;
}
const unregister = registerPeer(channelKey, peerId, target => {
runCoordinationRef.current?.(target, false);
});
reportValue(channelKey, peerId, committedValue);
return () => {
const handle = handleRef.current;
handleRef.current = null;
if (handle) {
handle.cancel();
}
inFlightTargetRef.current = {
has: false
};
unregister();
};
}, [channelKey, peerId]);
// Tell the coordinator our latest committed value so it can skip
// already-at-target peers when classifying barrier expectations.
React.useInsertionEffect(() => {
if (channelKey === null) {
return;
}
reportValue(channelKey, peerId, committedValue);
}, [channelKey, peerId, committedValue]);
const runCoordination = React.useCallback((target, isOriginator) => {
if (channelKey === null) {
// Keep `inFlightTargetRef` in sync so two synchronous calls
// like `setValue(p => p + 1); setValue(p => p + 1)` see the
// freshest base — `setPendingValue` is async, so the second
// updater would otherwise read the pre-render value and
// collapse to a single increment. The receiver-flow effect
// resets the ref on every external `underlyingValue` change,
// and `runCoordination` overwrites it on every subsequent
// call, so this won't leak across coordination cycles.
inFlightTargetRef.current = {
has: true,
value: target
};
setPendingValue(target);
setCommittedValue(target);
if (isOriginator) {
lastWrittenRef.current = {
has: true,
value: target
};
callbacksRef.current.setUnderlyingValue(target);
}
callbacksRef.current.onCommit?.(target, undefined);
return;
}
// Dedupe: if we're already coordinating exactly this target
// (e.g. a sibling-announce notification arrived after we
// already kicked off our own receiver-flow announce, our own
// originator-flow `announceTarget` call is still on the stack,
// or the user clicked the same value twice in quick
// succession), skip restarting the announcement. Without this
// the second call would cancel our in-flight handle and we'd
// lose our place in the barrier — or, when the dedupe also
// fires re-entrantly mid-announce, we'd recurse forever.
if (inFlightTargetRef.current.has && Object.is(inFlightTargetRef.current.value, target)) {
return;
}
// Supersede any in-flight announcement.
const previousHandle = handleRef.current;
if (previousHandle) {
previousHandle.cancel();
}
inFlightTargetRef.current = {
has: true,
value: target
};
// `pendingValue` is the user-facing "intent" signal and
// always flips synchronously so toolbars / pickers stay
// responsive on click. `isCoordinating` is what consumers
// typically gate an animation on — when
// `animateDuringPreload === false` we hold it until the
// originator's preload settles so a CPU-bound preload
// doesn't steal main-thread time from the animation that
// follows. The coordinator yields to the browser before
// invoking `preload`, so even sync preloads settle one
// macrotask later; receivers flip in the same tick
// regardless of the flag.
setPendingValue(target);
// Always clear stale waiting state from any previous
// coordination cycle synchronously — otherwise a later
// `onWaitingForPeers` (which fires from a timer relative to
// the announce) could be overwritten when we flip
// `isCoordinating`.
setIsWaitingForPeers(false);
const deferFlipForPreload = isOriginator && !timingRef.current.animateDuringPreload && callbacksRef.current.preload !== undefined;
let coordinatingFlipped = false;
const flipCoordinating = () => {
if (coordinatingFlipped) {
return;
}
coordinatingFlipped = true;
setIsCoordinating(true);
};
if (!deferFlipForPreload) {
flipCoordinating();
}
const userPreload = callbacksRef.current.preload;
const flipWrappedPreload = deferFlipForPreload ? (preloadTarget, signal) => {
// Wrap the user's preload so we flip `isCoordinating`
// the instant it settles — whether sync or async —
// but not before. Errors still flip; the engine logs
// them and treats the result as `undefined`, and the
// consumer expects the signal to converge regardless
// of the preload outcome.
let result;
let threw;
let didThrow = false;
try {
result = userPreload(preloadTarget, signal);
} catch (err) {
didThrow = true;
threw = err;
}
const isThenable = !didThrow && result !== null && result !== undefined && typeof result.then === 'function';
if (!isThenable) {
if (!signal.aborted) {
flipCoordinating();
}
if (didThrow) {
throw threw;
}
return result;
}
return result.then(value => {
if (!signal.aborted) {
flipCoordinating();
}
return value;
}, err => {
if (!signal.aborted) {
flipCoordinating();
}
throw err;
});
} : userPreload;
// Automatically hold the *commit* until the page's initial layout-shift
// sources have settled (see `layoutShiftGate`), for layout-shifting
// targets only. The consumer's preload still starts immediately and flips
// `isCoordinating` on its own settle — the gate wait runs in parallel and
// only delays the commit, so the first page-wide transform/variant change
// lands as one unified update. A no-op when nothing has registered with
// the gate (`whenLayoutShiftsSettled` returns `null`), so plain
// `useCoordinated` consumers are unaffected.
let wrappedPreload;
if (userPreload) {
wrappedPreload = (preloadTarget, signal) => {
const inner = flipWrappedPreload(preloadTarget, signal);
const gateWait = callbacksRef.current.causesLayoutShift(preloadTarget) ? whenLayoutShiftsSettled(signal) : null;
if (gateWait === null) {
return inner;
}
return Promise.all([gateWait, Promise.resolve(inner)]).then(([, result]) => result);
};
} else if (!layoutShiftsSettled() && callbacksRef.current.causesLayoutShift(target)) {
// No user preload, but the target shifts layout and the gate is still
// closed: synthesize a preload that only awaits the gate so the commit
// still holds until the page settles (the gate is otherwise consulted
// only inside the user-preload wrapper, so without this branch a
// layout-shifting peer with no preload would skip coordination entirely).
// Once the gate has opened
// this branch is skipped, so steady-state layout-shifting commits keep
// the synchronous fast path. The gate wait rejects `AbortError` on
// supersede, which propagates into the engine's `await preload(...)`
// and is swallowed there because the signal is aborted — exactly like
// the user-preload path above.
wrappedPreload = (preloadTarget, signal) => {
const gateWait = callbacksRef.current.causesLayoutShift(preloadTarget) ? whenLayoutShiftsSettled(signal) : null;
if (gateWait === null) {
return undefined;
}
return gateWait.then(() => undefined);
};
}
const handle = announceTarget(channelKey, peerId, target, {
causesLayoutShift: callbacksRef.current.causesLayoutShift,
preload: wrappedPreload,
onCommit: (committedTarget, preloaded) => {
// Side-effect first so consumers can install precomputed
// payloads before the value flip becomes visible.
callbacksRef.current.onCommit?.(committedTarget, preloaded);
// If the engine force-resolved the barrier while our
// deferred preload was still in flight (e.g. at
// `ultimateTimeoutMs`), `flipCoordinating` may not have
// run — flush it now so `isCoordinating` reaches the
// true → false transition consumers expect at least
// once per cycle.
flipCoordinating();
if (isOriginator) {
lastWrittenRef.current = {
has: true,
value: committedTarget
};
callbacksRef.current.setUnderlyingValue(committedTarget);
}
setCommittedValue(committedTarget);
// Clear coordination flags synchronously alongside the
// value flip so the next render reflects both at once.
setIsCoordinating(false);
setIsWaitingForPeers(false);
},
minWaitMs: timingRef.current.minWaitMs,
multiPeerExtraMinWaitMs: timingRef.current.multiPeerExtraMinWaitMs,
lazyMinWaitMs: timingRef.current.lazyMinWaitMs,
gracePeriodMs: timingRef.current.gracePeriodMs,
ultimateTimeoutMs: timingRef.current.ultimateTimeoutMs,
lazyCommitPriority: timingRef.current.lazyCommitPriority,
onWaitingForPeers: () => {
setIsWaitingForPeers(true);
},
isOriginator,
// Non-originators (e.g. storage echoes) anchor to the
// originator's announce time if a barrier is already open
// so late joiners share the same wall-clock deadline.
announceTime: !isOriginator && getBarrierAnnounceTime(channelKey, target) || Date.now()
});
handleRef.current = handle;
handle.settled.then(() => {
if (handleRef.current === handle) {
handleRef.current = null;
inFlightTargetRef.current = {
has: false
};
setIsCoordinating(false);
setIsWaitingForPeers(false);
}
});
}, [channelKey, peerId]);
// Keep the ref pointed at the latest `runCoordination` so the
// peer-registration callback (set once at mount) always invokes
// the current closure.
// eslint-disable-next-line react-hooks/refs -- forward-declared latest-ref: the once-registered peer callback must invoke the current runCoordination closure without re-registering
runCoordinationRef.current = runCoordination;
// Receiver flow: external `underlyingValue` change that we did not
// originate. Trigger a local coordination so this tab's peers commit
// together. Uses `useLayoutEffect` so the announcement lands in the
// same synchronous flush as the originator's broadcast — otherwise
// a sibling peer's `runCoordination` would slip past whatever
// `setTimeout` cadence the test (or browser) is driving and miss
// the originator's barrier window.
React.useLayoutEffect(() => {
if (channelKey === null) {
// Coordination disabled: the hook is a transparent pass-through,
// so the visible value / pending value are derived directly from
// `underlyingValue` at the return rather than mirrored into state.
// Only reset the in-flight ref here.
inFlightTargetRef.current = {
has: false
};
return;
}
if (lastWrittenRef.current.has) {
if (Object.is(lastWrittenRef.current.value, underlyingValue)) {
// Echo of our own write — already coordinated. Treat the
// sentinel as one-shot: consume it here so a *subsequent*
// external write that happens to round-trip to the same
// value (e.g. local→`b`, external→`c`, external→`b`) is
// recognized as a genuine external change rather than
// misclassified as another echo.
lastWrittenRef.current = {
has: false
};
return;
}
// Sentinel is stale: the underlying moved to something other
// than what we last wrote, so an earlier no-op originator
// write (which produced no echo) must have left the sentinel
// armed. Clear it now so a later external round-trip back to
// that value isn't suppressed.
lastWrittenRef.current = {
has: false
};
}
if (handleRef.current && inFlightTargetRef.current.has && Object.is(inFlightTargetRef.current.value, underlyingValue) && Object.is(pendingValue, underlyingValue)) {
// Already coordinating this target.
return;
}
if (Object.is(committedValue, underlyingValue)) {
// No external change to react to. We deliberately skip running
// `preload` on mount when the underlying value already matches
// our committed value: preload only needs to run when the value
// changes out from under us without the consumer calling the
// setter (e.g. a hydration write from `localStorage` or a
// parent state update). After the consumer interacts with the
// setter, the originator flow drives every cycle.
return;
}
// eslint-disable-next-line react-hooks/set-state-in-effect -- receiver flow: runCoordination drives the coordination state machine in response to an external underlyingValue change; this is the genuine effect side-effect, not derivable during render
runCoordination(underlyingValue, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [channelKey, underlyingValue, runCoordination]);
const coordinatedSetValue = React.useCallback(action => {
// Functional updaters need to see the latest target — even
// before React has re-rendered with the new `pendingValue` —
// so that bursts like `setValue(p => p + 1); setValue(p => p +
// 1)` compose to +2 instead of +1. `inFlightTargetRef` is
// updated synchronously inside `runCoordination` before
// `announceTarget` runs, so it's the freshest source.
const base = inFlightTargetRef.current.has ? inFlightTargetRef.current.value : pendingValue;
const next = typeof action === 'function' ? action(base) : action;
runCoordination(next, true);
}, [pendingValue, runCoordination]);
const extras = React.useMemo(() => ({
pendingValue,
isCoordinating,
isWaitingForPeers
}), [pendingValue, isCoordinating, isWaitingForPeers]);
// When coordination is disabled the hook is a transparent
// pass-through: derive the visible value, pending value, and inert
// coordination flags straight from `underlyingValue` rather than
// mirroring it into `committedValue` / `pendingValue` state.
if (channelKey === null) {
return [underlyingValue, coordinatedSetValue, {
pendingValue: underlyingValue,
isCoordinating: false,
isWaitingForPeers: false
}];
}
return [committedValue, coordinatedSetValue, extras];
}