Warning
This is an internal project, and is not intended for public use. No support or stability guarantees are provided.
The useCoordinated hook coordinates a piece of state across sibling component instances on a shared channel so that visually disruptive value changes commit in a single layout pass instead of independently. It wraps any useState-shaped primitive — useState, useLocalStorageState, usePreference, etc. — and adds a phased commit pipeline driven by the generic coordinatePreference engine.
Three panels are tied to a single preference object with two dimensions, each with very different fetch latencies (200 ms / 700 ms / 1.5 s):
compact / comfortable / spacious) — changes line count, so card heights change. Layout shift.A→Z / Z→A) — reorders the same lines. No layout shift.Without useCoordinated, both kinds of change cascade. Density flips jolt the page three times per click. Sort flips look ragged too, but at least nothing reflows.
With useCoordinated, the channel uses causesLayoutShift: (target) => target.density !== currentDensity to decide per-transition: density flips wait for the slowest peer and land together, but sort flips skip the barrier and commit eagerly as each peer's preload returns.
Each card fetches and commits independently. Sort changes look ragged but harmless. Density changes are visibly bad — the page jolts three times per click as heights flip in a cascade.
Density: compact, sort: A→Z
Density: compact, sort: A→Z
Density: compact, sort: A→Z
'use client';
import * as React from 'react';
import { useCoordinated } from '@mui/internal-docs-infra/useCoordinated';
import styles from './CascadingLoads.module.css';
type Density = 'compact' | 'comfortable' | 'spacious';
type Sort = 'asc' | 'desc';
type Preference = { density: Density; sort: Sort };
type Mode = 'uncoordinated' | 'coordinated';
const DENSITIES: Density[] = ['compact', 'comfortable', 'spacious'];
const SORTS: { value: Sort; label: string }[] = [
{ value: 'asc', label: 'A → Z' },
{ value: 'desc', label: 'Z → A' },
];
// Same data set rendered at three densities. Line count differs per
// density so card heights change when density flips — but only when
// density flips. Re-sorting a card never changes its height; that's
// what `causesLayoutShift` is going to lean on below.
const PANELS = {
Search: {
compact: ['12 results'],
comfortable: ['12 results', 'Sorted by relevance', 'Filtered: docs only'],
spacious: [
'12 results',
'Sorted by relevance',
'Filtered: docs only',
'Last refreshed: just now',
'Tip: press / to jump back to the search box',
],
},
Inbox: {
compact: ['3 unread'],
comfortable: ['3 unread', '1 mention', 'Snoozed: 2'],
spacious: [
'3 unread',
'1 mention',
'Snoozed: 2',
'You replied within 2h on average this week',
'Drafts auto-saved: 4',
],
},
Activity: {
compact: ['5 events'],
comfortable: ['5 events', '2 reviews requested', 'No failing checks'],
spacious: [
'5 events',
'2 reviews requested',
'No failing checks',
'Your streak: 7 days',
'Most active repo: docs-infra',
'Quietest day this week: Wednesday',
],
},
} as const;
type PanelName = keyof typeof PANELS;
const PANEL_NAMES: PanelName[] = ['Search', 'Inbox', 'Activity'];
// Wildly varied latencies make the difference between cascading and
// coordinated commits obvious. Click density and you can watch each
// strategy play out for several seconds.
const FETCH_DELAYS: Record<PanelName, number> = {
Search: 200,
Inbox: 700,
Activity: 1500,
};
function resolveLines(panel: PanelName, preference: Preference): readonly string[] {
const base = PANELS[panel][preference.density];
return preference.sort === 'asc' ? base : [...base].reverse();
}
function fetchLines(
panel: PanelName,
preference: Preference,
signal: AbortSignal,
): Promise<readonly string[]> {
return new Promise((resolve, reject) => {
const id = setTimeout(() => resolve(resolveLines(panel, preference)), FETCH_DELAYS[panel]);
signal.addEventListener('abort', () => {
clearTimeout(id);
reject(new Error('aborted'));
});
});
}
const MODE_QUALITY: Record<Mode, 'bad' | 'good'> = {
uncoordinated: 'bad',
coordinated: 'good',
};
const MODE_LABELS: Record<Mode, string> = {
uncoordinated: 'Without useCoordinated',
coordinated: 'With useCoordinated',
};
const MODE_BADGES: Record<Mode, string> = {
uncoordinated: 'Problem',
coordinated: 'Fix',
};
const MODE_DESCRIPTIONS: Record<Mode, string> = {
uncoordinated:
'Each card fetches and commits independently. Sort changes look ragged but harmless. Density changes are visibly bad — the page jolts three times per click as heights flip in a cascade.',
coordinated:
'All three cards share a channel. The barrier only engages when the new value would change layout: density flips wait for the slowest peer and land together, but sort flips commit eagerly per peer because nothing reflows.',
};
function CardChrome({
panel,
visiblePreference,
lines,
loading,
pendingHint,
}: {
panel: PanelName;
visiblePreference: Preference;
lines: readonly string[];
loading: boolean;
pendingHint?: string | null;
}) {
return (
<div className={styles.card} data-loading={loading || undefined}>
<div className={styles.cardHeader}>
<span className={styles.cardTitle}>{panel}</span>
<span className={styles.cardLatency}>{FETCH_DELAYS[panel]} ms fetch</span>
</div>
{loading ? <span className={styles.cardBadge}>Loading…</span> : null}
<p className={styles.cardDensity}>
Density: <strong>{visiblePreference.density}</strong>, sort:{' '}
<strong>{visiblePreference.sort === 'asc' ? 'A→Z' : 'Z→A'}</strong>
{pendingHint ? <span className={styles.cardPending}>{pendingHint}</span> : null}
</p>
<ul className={styles.cardLines}>
{lines.map((line) => (
<li key={`${panel}-${line}`}>{line}</li>
))}
</ul>
</div>
);
}
function describePending(current: Preference, target: Preference): string | null {
if (current.density !== target.density && current.sort !== target.sort) {
return `→ ${target.density}, ${target.sort === 'asc' ? 'A→Z' : 'Z→A'}`;
}
if (current.density !== target.density) {
return `→ ${target.density}`;
}
if (current.sort !== target.sort) {
return `→ ${target.sort === 'asc' ? 'A→Z' : 'Z→A'}`;
}
return null;
}
// ---------- "Problem": no coordination ----------
function UncoordinatedCard({ panel, preference }: { panel: PanelName; preference: Preference }) {
const [lines, setLines] = React.useState<readonly string[]>(() =>
resolveLines(panel, preference),
);
const [visiblePreference, setVisiblePreference] = React.useState<Preference>(preference);
const [loading, setLoading] = React.useState(false);
React.useEffect(() => {
if (
preference.density === visiblePreference.density &&
preference.sort === visiblePreference.sort
) {
return undefined;
}
const controller = new AbortController();
setLoading(true);
fetchLines(panel, preference, controller.signal).then(
(next) => {
setLines(next);
setVisiblePreference(preference);
setLoading(false);
},
() => {
// Aborted by the next change.
},
);
return () => controller.abort();
}, [panel, preference, visiblePreference]);
return (
<CardChrome
panel={panel}
visiblePreference={visiblePreference}
lines={lines}
loading={loading}
pendingHint={describePending(visiblePreference, preference)}
/>
);
}
// ---------- "Fix": coordinated ----------
function CoordinatedCard({
panel,
preference,
onChangePreference,
}: {
panel: PanelName;
preference: Preference;
onChangePreference: (next: Preference) => void;
}) {
const tuple = React.useMemo<[Preference, (next: Preference) => void]>(
() => [preference, onChangePreference],
[preference, onChangePreference],
);
const [lines, setLines] = React.useState<readonly string[]>(() =>
resolveLines(panel, preference),
);
// Track the most-recently committed density so causesLayoutShift
// can compare target vs current without depending on stale closure
// state. The hook stores the option in a ref, so this ref read
// always sees the latest committed density.
const lastCommittedDensityRef = React.useRef<Density>(preference.density);
const [visiblePreference, , extras] = useCoordinated<Preference, readonly string[]>(tuple, {
channelKey: 'cascading-loads-demo',
peerId: `coord-${panel}`,
// The whole point of this demo: only ask peers to wait when the
// transition actually changes layout. Sort flips render the same
// number of lines at the same density, so they skip the barrier
// and commit as soon as each peer's own preload resolves.
causesLayoutShift: (target) => target.density !== lastCommittedDensityRef.current,
preload: (target, signal) => fetchLines(panel, target, signal),
// The preload is an I/O fetch — overlap the card's loading
// indicator with the network roundtrip rather than waiting for
// it to finish.
animateDuringPreload: true,
// The lazy commit itself is cheap (a `setLines` call), so skip
// the `requestIdleCallback` defer — each panel's swap should
// land as soon as its own fetch resolves, not cluster near the
// slowest peer's settle.
lazyCommitPriority: 'normal',
onCommit: (target, preloaded) => {
lastCommittedDensityRef.current = target.density;
if (preloaded) {
setLines(preloaded);
}
},
// Long enough to absorb the 1500 ms slow peer.
ultimateTimeoutMs: 4000,
gracePeriodMs: 1500,
});
return (
<CardChrome
panel={panel}
visiblePreference={visiblePreference}
lines={lines}
loading={extras.isCoordinating}
pendingHint={describePending(visiblePreference, extras.pendingValue)}
/>
);
}
function Cards({
mode,
preference,
onChangePreference,
}: {
mode: Mode;
preference: Preference;
onChangePreference: (next: Preference) => void;
}) {
if (mode === 'uncoordinated') {
return (
<div className={styles.cards}>
{PANEL_NAMES.map((panel) => (
<UncoordinatedCard key={panel} panel={panel} preference={preference} />
))}
</div>
);
}
return (
<div className={styles.cards}>
{PANEL_NAMES.map((panel) => (
<CoordinatedCard
key={panel}
panel={panel}
preference={preference}
onChangePreference={onChangePreference}
/>
))}
</div>
);
}
export function CascadingLoads() {
const [mode, setMode] = React.useState<Mode>('uncoordinated');
const [preference, setPreference] = React.useState<Preference>({
density: 'compact',
sort: 'asc',
});
return (
<div className={styles.root} data-mode={mode}>
<div className={styles.controls}>
<fieldset className={styles.modePicker}>
<legend className={styles.modeLegend}>Coordination mode</legend>
{(Object.keys(MODE_LABELS) as Mode[]).map((value) => (
<label
key={value}
className={styles.modeOption}
data-active={value === mode}
data-quality={MODE_QUALITY[value]}
>
<input
type="radio"
name="cascading-loads-mode"
value={value}
checked={value === mode}
onChange={() => setMode(value)}
/>
<span className={styles.modeBadge}>{MODE_BADGES[value]}</span>
<span>{MODE_LABELS[value]}</span>
</label>
))}
</fieldset>
</div>
<div className={styles.preferenceBar}>
<div className={styles.preferenceGroup} data-shift="yes">
<span className={styles.preferenceLabel}>
Density <em>(causes layout shift)</em>
</span>
{DENSITIES.map((option) => (
<button
key={option}
type="button"
onClick={() => setPreference((prev) => ({ ...prev, density: option }))}
className={
preference.density === option
? `${styles.densityButton} ${styles.densityButtonSelected}`
: styles.densityButton
}
>
{option}
</button>
))}
</div>
<div className={styles.preferenceGroup} data-shift="no">
<span className={styles.preferenceLabel}>
Sort <em>(no layout shift)</em>
</span>
{SORTS.map((option) => (
<button
key={option.value}
type="button"
onClick={() => setPreference((prev) => ({ ...prev, sort: option.value }))}
className={
preference.sort === option.value
? `${styles.densityButton} ${styles.densityButtonSelected}`
: styles.densityButton
}
>
{option.label}
</button>
))}
</div>
</div>
<p className={styles.hint} data-quality={MODE_QUALITY[mode]}>
{MODE_DESCRIPTIONS[mode]}
</p>
<Cards key={mode} mode={mode} preference={preference} onChangePreference={setPreference} />
</div>
);
}The minimum wiring is just a useState tuple plus a channelKey and causesLayoutShift:
import { useCoordinated } from '@mui/internal-docs-infra/useCoordinated';
function MyView({ value, setValue }: { value: string; setValue: (next: string) => void }) {
const [visibleValue, setVisible, extras] = useCoordinated([value, setValue], {
channelKey: 'my-view',
causesLayoutShift: () => true,
});
return (
<section data-coordinating={extras.isCoordinating || undefined}>
{visibleValue}
{extras.pendingValue !== visibleValue ? ` → ${extras.pendingValue}` : null}
</section>
);
}Pass channelKey: null to disable coordination — the hook becomes a transparent pass-through.
Funnelling every change into a single paint costs responsiveness: every peer waits for the slowest preload before any of them commits. For changes that don't actually reshape the layout that's the wrong tradeoff — a badge, an icon swap, a sort reorder, or anything happening off-screen could safely flip on the spot and the user would never see a tear.
useCoordinated's escape hatch is causesLayoutShift. Return false for a target and that peer skips the barrier entirely: it commits as soon as its own preload finishes, on a per-peer serial chain that's idle-callback-paced so it never fights the main thread. Return true and the peer joins the shared barrier so it lands together with its siblings.
The decision is per-announcement and per-peer, so a single channel can carry a mix of barrier and lazy traffic at once. The cascading-loads demo above already does this on one channel: density flips reshape card height and take the barrier, while sort flips reorder the same rows and skip it. Heavier lazy work (large lists, off-screen panels, expensive renders) benefits the most — it can take as long as it needs without holding the visible reflowing peers hostage.
The result is the best of both worlds: layout shifts batch into a single paint, but everything else stays lazy.
Wrap an existing [value, setValue] tuple to make any change show up everywhere at once.
import { useCoordinated } from '@mui/internal-docs-infra/useCoordinated';
import { useLocalStorageState } from '@mui/internal-docs-infra/useLocalStorageState';
function ThemePanel() {
const underlying = useLocalStorageState('theme', ['light', 'dark'], () => 'light');
const [theme, setTheme, extras] = useCoordinated(underlying, {
channelKey: 'theme',
causesLayoutShift: () => true,
});
return (
<button type="button" onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
{theme} {extras.isCoordinating ? '(syncing…)' : ''}
</button>
);
}The bundled useCoordinatedLocalStorage and useCoordinatedPreference compose these two primitives for you.
preload runs while sibling peers are still waiting. Whatever it returns is handed to onCommit so the new data lands in the same frame the value flips.
const [variant, setVariant, extras] = useCoordinated(underlying, {
channelKey: 'demo',
causesLayoutShift: () => true,
preload: async (target, signal) => {
const res = await fetch(`/code/${target}`, { signal });
return res.json();
},
onCommit: (target, payload) => {
setPrecomputed(payload); // installed atomically with the value flip
},
});If a newer announcement supersedes yours, the previous AbortSignal is aborted so you can cancel network work.
isCoordinating is the signal most consumers drive an animation off — a fade, a skeleton, a "loading" badge, a data-busy attribute. When that animation runs matters depending on what preload is doing:
localStorage deserialize) — the main thread is idle while you wait for bytes. You want the animation and the I/O roundtrip to overlap so the user sees feedback the instant they click.animateDuringPreload controls this. It defaults to false — the safer choice for CPU work — and you opt in with true when the preload is I/O-bound:
const [variant, , extras] = useCoordinated(underlying, {
channelKey: 'demo',
causesLayoutShift: () => true,
// Fetch is I/O-bound — overlap the badge with the network roundtrip.
animateDuringPreload: true,
preload: async (target, signal) => fetch(`/code/${target}`, { signal }).then((r) => r.json()),
});
return <Badge loading={extras.isCoordinating} />pendingValue (the user-facing "intent" signal toolbars and pickers key off) always flips synchronously on click regardless of this flag, so the picker stays responsive in both modes. With animateDuringPreload: true, isCoordinating also flips in the same tick. With the default animateDuringPreload: false, the coordinator yields to the browser before invoking preload so any intermediate UI can paint, so even a synchronous preload lets isCoordinating settle one macrotask after the click; when preload is omitted the flip is fully synchronous.
Return false from causesLayoutShift for changes that don't move any pixels. The peer skips the barrier and commits on its own serial chain so it never holds up its neighbors.
const [tooltipText, setTooltipText] = useCoordinated(underlying, {
channelKey: 'tooltips',
causesLayoutShift: () => false,
preload: loadTranslation, // serialized per-peer, not across siblings
});This is the pattern that justifies the whole hook: a single source of truth where some announcements reshape the layout and some don't. Cascading-loads above is built on this — its density dimension takes the barrier path and its sort dimension takes the lazy path, on the same channel.
Two rules make this work:
channelKey per piece of coordinated state, not one per visual treatment. Every peer that should hear about a change joins the same channel, including peers that won't reflow. That's how you keep them in sync; mixing channels would let subscribers drift on stale tabs, cross-tab updates, or aborted fetches.causesLayoutShift is a per-announcement decision: a peer can return false for some targets (e.g. switching between two same-width skins) and true for others (e.g. switching to a skin with extra info). The engine routes each announcement independently.causesLayoutShift is called fresh on every announcement, so it can read mutable local state. The classic case: an accordion whose body resizes when expanded but whose header is the same height regardless. Open accordions should join the barrier; closed ones should flip on the spot.
Each section reads its current open flag from a ref inside causesLayoutShift: () => openRef.current. The same preference change is routed per-peer:
false → every peer commits immediately. The one-line preview under each header changes text but never reflows.Open one or two sections, change the detail level, and watch the closed sections flip ahead of the open sections' bodies — same channel, different routing.
Try this: first switch the preference with all sections closed. Every section is on the lazy path — its one-line preview updates immediately and never reflows. Then expand Network and Storage, leave Batteryclosed, and switch again: the two open sections wait for each other and flip together, while the closed Battery section's update is deferred (shown here with an exaggerated 800ms delay to demonstrate that lazy peers wait for the barrier to finish painting before they update). In a real app, this deferral would be just one macrotask, keeping the main thread clear for layout work.
OK.
64% used.
78%.
'use client';
import * as React from 'react';
import { useCoordinated } from '@mui/internal-docs-infra/useCoordinated';
import styles from './ConditionalShift.module.css';
type Pref = 'brief' | 'normal' | 'verbose';
const PREFS: Pref[] = ['brief', 'normal', 'verbose'];
type SectionName = 'Network' | 'Storage' | 'Battery';
// The whole point of the demo: the body length per preference is
// very different, so an *open* section visibly resizes when the
// preference flips. A *closed* section's header height never
// changes, so it can flip without waiting.
const BODIES: Record<SectionName, Record<Pref, string>> = {
Network: {
brief: 'OK.',
normal: 'Connected via Wi-Fi. 45 ms round trip to the gateway.',
verbose:
'Connected via Wi-Fi on 5 GHz channel 36. Round trip to gateway 45 ms; ' +
'to 1.1.1.1 18 ms. IPv6 active. Last reconnection 3 h 12 m ago. ' +
'Packet loss in the last 5 minutes: 0.',
},
Storage: {
brief: '64% used.',
normal: '320 GB used of 500 GB. About 180 GB free.',
verbose:
'320 GB used of 500 GB (64%). Largest categories: Photos 142 GB, ' +
'System 38 GB, Applications 26 GB. Trash holds 4.1 GB that can be ' +
'reclaimed. Last snapshot taken 12 minutes ago.',
},
Battery: {
brief: '78%.',
normal: 'At 78%, discharging. About 4h 20m remaining.',
verbose:
'At 78%, currently discharging at 6.4 W. About 4h 20m remaining at ' +
'this rate. Cycle count 312 of an expected 1000. Design capacity ' +
'retained: 94%. Charger last connected 1h 04m ago.',
},
};
// Different per-section "preload" latencies make the barrier
// coordination visible when more than one section is open.
const PRELOAD_MS: Record<SectionName, number> = {
Network: 600,
Storage: 250,
Battery: 80,
};
// Artificial delay for lazy peers to visually demonstrate that they wait
// for the barrier to commit before applying their updates. In production,
// this delay would be ~0ms (just one macrotask hop via setTimeout(0)).
// Here we exaggerate it to 800ms so the demo clearly shows the deferral.
const LAZY_DEFERRAL_DISPLAY_MS = 800;
function Section({
name,
pref,
onChangePref,
}: {
name: SectionName;
pref: Pref;
onChangePref: (next: Pref) => void;
}) {
const [open, setOpen] = React.useState(false);
// Track the delayed update for lazy peers to visually demonstrate deferral
const [delayedCommitId, setDelayedCommitId] = React.useState<NodeJS.Timeout | null>(null);
// `causesLayoutShift` / `preload` are captured by the engine when
// an announcement is made. Reading the latest open-state out of a
// ref keeps the routing decision fresh without churning the
// callbacks (which would restart in-flight barriers).
const openRef = React.useRef(open);
openRef.current = open;
const tuple = React.useMemo<[Pref, (next: Pref) => void]>(
() => [pref, onChangePref],
[pref, onChangePref],
);
const [visiblePref, , extras] = useCoordinated<Pref, void>(tuple, {
channelKey: 'conditional-shift-demo',
peerId: `section-${name}`,
// The same announcement is routed differently per peer, based
// on whether THIS section is currently expanded:
// - open → barrier path: wait for sibling open sections to
// finish their preloads, then flip together.
// - closed → lazy path: commit immediately; never block the
// barrier the open sections are running.
causesLayoutShift: () => openRef.current,
// Loading affordance overlaps with the simulated preload window,
// so the section's `data-pending` highlight is visible the moment
// the user clicks rather than appearing after preload settles.
animateDuringPreload: true,
// The simulated preload is I/O-shaped (a timer standing in for a
// fetch). Skip the idle commit so closed (lazy) sections flip as
// soon as their own delay elapses, instead of waiting for a
// browser-scheduled idle slot.
lazyCommitPriority: 'normal',
// Simulated work that only matters when we're going to repaint
// the body. Closed peers skip waiting and resolve immediately.
preload: (_target, signal) =>
new Promise<void>((resolve, reject) => {
if (!openRef.current) {
// For lazy peers, add an artificial display delay to visually
// demonstrate that they wait until after the barrier commits.
// In production, this would be ~0ms (one macrotask via setTimeout).
const id = setTimeout(resolve, LAZY_DEFERRAL_DISPLAY_MS);
setDelayedCommitId(id);
signal.addEventListener('abort', () => {
clearTimeout(id);
setDelayedCommitId(null);
reject(new Error('aborted'));
});
return;
}
const id = setTimeout(resolve, PRELOAD_MS[name]);
signal.addEventListener('abort', () => {
clearTimeout(id);
reject(new Error('aborted'));
});
}),
gracePeriodMs: 500,
});
React.useEffect(() => {
// Cleanup any pending delayed commit if component unmounts
return () => {
if (delayedCommitId !== null) {
clearTimeout(delayedCommitId);
}
};
}, [delayedCommitId]);
// The badge plus the one-line preview are always visible — even
// when the section is collapsed. That's the cue: if you flip the
// preference while this section is closed, both update *now*
// (single-line text changes, no reflow); if it's open, they hold
// with the rest of the open sections until they all flip together.
const badge = (
<span
className={styles.badge}
data-pending={extras.isCoordinating || undefined}
title={
extras.pendingValue !== visiblePref
? `pending: ${extras.pendingValue}`
: `current: ${visiblePref}`
}
>
{visiblePref}
</span>
);
return (
<div className={styles.section} data-open={open || undefined}>
<button
type="button"
className={styles.summary}
aria-expanded={open}
onClick={() => setOpen((prev) => !prev)}
>
<span className={styles.chev} aria-hidden>
{open ? '▾' : '▸'}
</span>
<span className={styles.sectionName}>{name}</span>
{badge}
{extras.isCoordinating ? (
<span className={styles.spinner} aria-live="polite">
{extras.isWaitingForPeers ? 'waiting…' : 'loading…'}
</span>
) : null}
<span className={styles.routeHint}>
{open ? 'barrier (waits for other open sections)' : 'lazy (no barrier)'}
</span>
</button>
{open ? (
<p className={styles.body}>{BODIES[name][visiblePref]}</p>
) : (
<p className={styles.preview} title={BODIES[name][visiblePref]}>
{BODIES[name][visiblePref]}
</p>
)}
</div>
);
}
export function ConditionalShift() {
// One shared preference. Every section subscribes via
// `useCoordinated`, but each one decides per-announcement whether
// the change reshapes its layout — based on whether it's open.
const [pref, setPref] = React.useState<Pref>('brief');
return (
<div className={styles.container}>
<p className={styles.intro}>
<strong>Try this:</strong> first switch the preference with all sections closed. Every
section is on the lazy path — its one-line preview updates immediately and never reflows.
Then expand <em>Network</em> and <em>Storage</em>, leave <em>Battery</em> closed, and switch
again: the two open sections wait for each other and flip together, while the closed Battery
section's update is deferred (shown here with an exaggerated 800ms delay to demonstrate
that lazy peers wait for the barrier to finish painting before they update). In a real app,
this deferral would be just one macrotask, keeping the main thread clear for layout work.
</p>
<div className={styles.toolbar}>
<span className={styles.label}>Detail:</span>
{PREFS.map((option) => (
<button
key={option}
type="button"
onClick={() => setPref(option)}
className={
pref === option ? `${styles.toggle} ${styles.toggleSelected}` : styles.toggle
}
>
{option}
</button>
))}
</div>
<div className={styles.list}>
{(['Network', 'Storage', 'Battery'] as SectionName[]).map((name) => (
<Section key={name} name={name} pref={pref} onChangePref={setPref} />
))}
</div>
</div>
);
}If the originator's peers don't all check in within gracePeriodMs, isWaitingForPeers flips to true on the originator. Surface it as a spinner so users know the slow card is the bottleneck.
const [tab, setTab, { isCoordinating, isWaitingForPeers }] = useCoordinated(...);
return (
<button type="button" data-busy={isCoordinating}>
{isWaitingForPeers ? 'Waiting for other panels…' : 'Switch tab'}
</button>
);Not every preference change involves a network fetch — sometimes the cost is purely synchronous render work (re-highlighting many code blocks, re-laying out a virtualized tree, recomputing a chart). If every panel does that work inline on the next render, React commits the new state immediately but the layout pass blocks the main thread for the sum of every panel's work. The button never visually updates; the page just freezes.
useCoordinated's preload is the right place to move that work. Chunk it (yield to the event loop between slices, or hand it to a worker) so the toolbar's pendingValue paints in the same frame as the click. Once every peer's preload finishes, the coordinated commit installs the precomputed results in a single render.
Four panels each burn between 80 ms and 320 ms of synchronous CPU per theme change. In the Problem mode that work runs inline on render — clicking a theme freezes the page for ~750 ms, and the toolbar's selected-button state can't repaint until everything is done. In the Fix mode the same total work runs inside a yielding preload, the toolbar updates the moment you click, and the four panels paint together as soon as the slowest one finishes.
This demo opts in with animateDuringPreload: true so the per-panel "Computing…" badge is the visible illustration of the chunked preload — without the opt-in, the safer default would hide it until commit. For production CPU-bound code (the useCode transform pipeline does this) you typically want the default: no animation runs concurrently with the preload, so the transition starts cleanly once the work is done.
The new theme commits the moment you click, so every panel runs its sync CPU work in the same React render. The button stays pressed-down, the toolbar never updates its highlighted theme, and the page is frozen for ~750 ms before anything paints. Try hovering another button mid-burn — no feedback.
Theme: light
checksum: 0x00000000
Theme: light
checksum: 0x00000000
Theme: light
checksum: 0x00000000
Theme: light
checksum: 0x00000000
'use client';
import * as React from 'react';
import { useCoordinated } from '@mui/internal-docs-infra/useCoordinated';
import styles from './CpuBound.module.css';
type Theme = 'light' | 'dark' | 'high-contrast';
type Mode = 'uncoordinated' | 'coordinated';
const THEMES: Theme[] = ['light', 'dark', 'high-contrast'];
const PANEL_NAMES = ['Module A', 'Module B', 'Module C', 'Module D'] as const;
type PanelName = (typeof PANEL_NAMES)[number];
// Per-panel "work" — different sizes so the originator block lasts
// noticeably longer than the rest in the uncoordinated case. Numbers
// are deliberately large enough to be felt as a hitch but small
// enough that the demo never hangs the page for long.
const WORK_BUDGET_MS: Record<PanelName, number> = {
'Module A': 80,
'Module B': 220,
'Module C': 140,
'Module D': 320,
};
// Busy-loop synchronously for `budgetMs`. This is the worst-case
// shape of CPU-bound preference work: highlighting many code blocks,
// laying out a virtualized tree, etc. Run inline it pegs the main
// thread; run inside a yielding preload it never blocks paint.
function burnSync(budgetMs: number): string {
const start = performance.now();
// The work output: a fake "checksum" of the simulated computation.
let checksum = 0;
while (performance.now() - start < budgetMs) {
// Trivially non-optimizable arithmetic so the JIT can't elide it.
checksum = Math.trunc(checksum + Math.sin(checksum + 1) * 1e6);
}
return `0x${Math.abs(checksum).toString(16).padStart(8, '0')}`;
}
// Chunked variant: same total CPU budget but yields to the event
// loop between slices so React's layout passes, button hover, and
// the toolbar's pending state still feel responsive.
async function burnChunked(budgetMs: number, signal: AbortSignal, sliceMs = 16): Promise<string> {
const start = performance.now();
let checksum = 0;
while (performance.now() - start < budgetMs) {
if (signal.aborted) {
throw new Error('aborted');
}
const sliceStart = performance.now();
while (performance.now() - sliceStart < sliceMs && performance.now() - start < budgetMs) {
checksum = Math.trunc(checksum + Math.sin(checksum + 1) * 1e6);
}
// Hand the main thread back so paint, input, and other peers
// can interleave.
await new Promise<void>((resolve) => {
setTimeout(resolve, 0);
});
}
return `0x${Math.abs(checksum).toString(16).padStart(8, '0')}`;
}
const MODE_QUALITY: Record<Mode, 'bad' | 'good'> = {
uncoordinated: 'bad',
coordinated: 'good',
};
const MODE_LABELS: Record<Mode, string> = {
uncoordinated: 'Without useCoordinated',
coordinated: 'With useCoordinated',
};
const MODE_BADGES: Record<Mode, string> = {
uncoordinated: 'Problem',
coordinated: 'Fix',
};
const MODE_DESCRIPTIONS: Record<Mode, string> = {
uncoordinated:
'The new theme commits the moment you click, so every panel runs its sync CPU work in the same React render. The button stays pressed-down, the toolbar never updates its highlighted theme, and the page is frozen for ~750 ms before anything paints. Try hovering another button mid-burn — no feedback.',
coordinated:
'The toolbar flips pendingValue immediately so the new theme highlights in the same frame as the click. Each panel runs the same total work inside a yielding preload, the panels show a loading badge, and the four heavy renders all land together once the slowest one finishes — without ever blocking the main thread for more than one slice.',
};
function PanelChrome({
name,
theme,
checksum,
loading,
pendingHint,
}: {
name: PanelName;
theme: Theme;
checksum: string;
loading: boolean;
pendingHint?: string | null;
}) {
return (
<div className={styles.panel} data-theme={theme} data-loading={loading || undefined}>
<div className={styles.panelHeader}>
<span className={styles.panelTitle}>{name}</span>
<span className={styles.panelBudget}>{WORK_BUDGET_MS[name]} ms work</span>
</div>
{loading ? <span className={styles.panelBadge}>Computing…</span> : null}
<p className={styles.panelMeta}>
Theme: <strong>{theme}</strong>
{pendingHint ? <span className={styles.panelPending}>{pendingHint}</span> : null}
</p>
<p className={styles.panelChecksum}>checksum: {checksum}</p>
</div>
);
}
// ---------- "Problem": CPU work in render ----------
// The panel runs the sync busy-loop in a layout effect on every
// theme change. React commits the state, then the layout pass
// blocks until all four panels finish their inline work — and
// during that block the toolbar and button state can't repaint.
// (We use a layout effect rather than `useMemo` during render so
// the work never runs on the server, where it would both waste CPU
// and produce a hydration-mismatching checksum.)
function UncoordinatedPanel({ name, theme }: { name: PanelName; theme: Theme }) {
const [checksum, setChecksum] = React.useState<string>('0x00000000');
React.useLayoutEffect(() => {
setChecksum(burnSync(WORK_BUDGET_MS[name]));
}, [name, theme]);
return <PanelChrome name={name} theme={theme} checksum={checksum} loading={false} />;
}
// ---------- "Fix": CPU work in a yielding preload ----------
function CoordinatedPanel({
name,
theme,
onChangeTheme,
}: {
name: PanelName;
theme: Theme;
onChangeTheme: (next: Theme) => void;
}) {
const tuple = React.useMemo<[Theme, (next: Theme) => void]>(
() => [theme, onChangeTheme],
[theme, onChangeTheme],
);
const [checksum, setChecksum] = React.useState<string>(
() =>
// Initial paint isn't part of the demo — seed with a placeholder
// so we don't burn CPU on first mount.
'0x00000000',
);
const [visibleTheme, , extras] = useCoordinated<Theme, string>(tuple, {
channelKey: 'cpu-bound-demo',
peerId: `coord-${name}`,
causesLayoutShift: () => true,
// The CPU work runs inside `preload`, chunked so it yields back
// to the event loop between slices. The toolbar (and every
// pendingValue indicator) can repaint while the work continues;
// the coordinated commit installs the precomputed result
// atomically once every peer finishes.
preload: (_target, signal) => burnChunked(WORK_BUDGET_MS[name], signal),
// The "Computing…" badge IS the visible animation for this demo
// — we want it on screen while the chunked preload runs.
// Default (`false`) would hold the badge until preload settled,
// hiding the very state we're trying to illustrate.
animateDuringPreload: true,
onCommit: (_target, preloaded) => {
if (preloaded) {
setChecksum(preloaded);
}
},
ultimateTimeoutMs: 3000,
gracePeriodMs: 250,
});
return (
<PanelChrome
name={name}
theme={visibleTheme}
checksum={checksum}
loading={extras.isCoordinating}
pendingHint={extras.pendingValue !== visibleTheme ? `→ ${extras.pendingValue}` : null}
/>
);
}
function Panels({
mode,
theme,
onChangeTheme,
}: {
mode: Mode;
theme: Theme;
onChangeTheme: (next: Theme) => void;
}) {
if (mode === 'uncoordinated') {
return (
<div className={styles.panels}>
{PANEL_NAMES.map((name) => (
<UncoordinatedPanel key={name} name={name} theme={theme} />
))}
</div>
);
}
return (
<div className={styles.panels}>
{PANEL_NAMES.map((name) => (
<CoordinatedPanel key={name} name={name} theme={theme} onChangeTheme={onChangeTheme} />
))}
</div>
);
}
export function CpuBound() {
const [mode, setMode] = React.useState<Mode>('uncoordinated');
const [theme, setTheme] = React.useState<Theme>('light');
return (
<div className={styles.root} data-mode={mode}>
<div className={styles.controls}>
<fieldset className={styles.modePicker}>
<legend className={styles.modeLegend}>Coordination mode</legend>
{(Object.keys(MODE_LABELS) as Mode[]).map((value) => (
<label
key={value}
className={styles.modeOption}
data-active={value === mode}
data-quality={MODE_QUALITY[value]}
>
<input
type="radio"
name="cpu-bound-mode"
value={value}
checked={value === mode}
onChange={() => setMode(value)}
/>
<span className={styles.modeBadge}>{MODE_BADGES[value]}</span>
<span>{MODE_LABELS[value]}</span>
</label>
))}
</fieldset>
<div className={styles.themePicker}>
<span className={styles.themeLabel}>Theme:</span>
{THEMES.map((option) => (
<button
key={option}
type="button"
onClick={() => setTheme(option)}
className={
theme === option
? `${styles.themeButton} ${styles.themeButtonSelected}`
: styles.themeButton
}
>
{option}
</button>
))}
</div>
</div>
<p className={styles.hint} data-quality={MODE_QUALITY[mode]}>
{MODE_DESCRIPTIONS[mode]}
</p>
<Panels key={mode} mode={mode} theme={theme} onChangeTheme={setTheme} />
</div>
);
}useScrollAnchor when the layout shift is above the triggerThe coordinated commit means every peer flips in a single paint — but if those peers live above the button you clicked, that single paint also pushes the button down the page. Your cursor ends up over empty space, ready for an accidental click on whatever just slid under it.
Pair useCoordinated with useScrollAnchor: announce the change, anchor the button, and the page scrolls to keep the button under your cursor while the cards above it grow.
Three panels live inside a scrollable viewport with a "Change detail" toolbar sitting in normal flow below them. Scroll the panel so the toolbar sits near the bottom of the viewport, then change the detail level. Without an anchor, the coordinated commit makes the cards much taller and the toolbar slides off-screen; with useScrollAnchor pointed at the toolbar, it stays exactly under your cursor through the same coordinated commit.
const { containerRef, anchorScroll } = useScrollAnchor<HTMLDivElement>();
const toolbarRef = React.useRef<HTMLDivElement>(null);
function onChangeDetail(next: Detail) {
// Anchor first, then announce: the coordinated commit will
// resize the cards above the toolbar, and the anchor will
// scroll the page so `toolbarRef` stays pinned.
anchorScroll(toolbarRef.current, 1200);
setDetail(next);
}The cards already flip together thanks to useCoordinated — but the button you clicked is below them, so the commit pushes it down off-screen in a single jump. Your cursor sits where the button used to be.
'use client';
import * as React from 'react';
import { useCoordinated } from '@mui/internal-docs-infra/useCoordinated';
import { useScrollAnchor } from '@mui/internal-docs-infra/useScrollAnchor';
import styles from './AnchorPairing.module.css';
type Detail = 'summary' | 'expanded' | 'verbose';
type Mode = 'no-anchor' | 'with-anchor';
const DETAILS: Detail[] = ['summary', 'expanded', 'verbose'];
// The cards above the toggle button grow taller as detail level
// rises, so flipping detail pushes the button further down the
// page. The point of this demo: only the combination of
// useCoordinated (everyone flips at once → one anchor moment) and
// useScrollAnchor (pin the button across that moment) keeps the
// page calm for the user.
const PANELS = {
Releases: {
summary: ['v1.4.0 shipped'],
expanded: [
'v1.4.0 shipped',
'v1.4.0 — adds caching for type lookups',
'v1.3.2 — bug fix in MDX loader',
],
verbose: [
'v1.4.0 shipped',
'v1.4.0 — adds caching for type lookups',
'v1.3.2 — bug fix in MDX loader',
'v1.3.1 — perf: skip re-parsing unchanged source',
'v1.3.0 — new useCoordinated hook',
'v1.2.6 — types: narrow OnSiblingAnnounce',
'v1.2.5 — fix: race in barrier cancellation',
],
},
Issues: {
summary: ['8 open'],
expanded: [
'8 open',
'#411 — Type extraction times out on circular generics',
'#408 — Loader misses .mdx in nested routes',
],
verbose: [
'8 open',
'#411 — Type extraction times out on circular generics',
'#408 — Loader misses .mdx in nested routes',
'#405 — Code snippet copy-button steals focus',
'#403 — Live editor flashes on first paint',
'#401 — Demo iframe sandbox blocks fetch',
'#398 — Sidebar collapses on hover',
'#395 — Search results truncated on Safari',
],
},
Activity: {
summary: ['12 commits today'],
expanded: [
'12 commits today',
'Merged #410 — feat: precomputed types',
'Opened #412 — investigate flaky test',
],
verbose: [
'12 commits today',
'Merged #410 — feat: precomputed types',
'Opened #412 — investigate flaky test',
'Reviewed #407, #409, #410',
'Deployed preview to render-pr-1371',
'Updated 3 dependencies via renovate',
'Triaged 5 incoming issues',
],
},
} as const;
type PanelName = keyof typeof PANELS;
const PANEL_NAMES: PanelName[] = ['Releases', 'Issues', 'Activity'];
// Modest, varied latencies — just enough to make the coordinated
// commit feel batched.
const FETCH_DELAYS: Record<PanelName, number> = {
Releases: 150,
Issues: 350,
Activity: 550,
};
function fetchLines(
panel: PanelName,
detail: Detail,
signal: AbortSignal,
): Promise<readonly string[]> {
return new Promise((resolve, reject) => {
const id = setTimeout(() => resolve(PANELS[panel][detail]), FETCH_DELAYS[panel]);
signal.addEventListener('abort', () => {
clearTimeout(id);
reject(new Error('aborted'));
});
});
}
const MODE_QUALITY: Record<Mode, 'bad' | 'good'> = {
'no-anchor': 'bad',
'with-anchor': 'good',
};
const MODE_LABELS: Record<Mode, string> = {
'no-anchor': 'Only useCoordinated',
'with-anchor': 'useCoordinated + useScrollAnchor',
};
const MODE_BADGES: Record<Mode, string> = {
'no-anchor': 'Problem',
'with-anchor': 'Fix',
};
const MODE_DESCRIPTIONS: Record<Mode, string> = {
'no-anchor':
'The cards already flip together thanks to useCoordinated — but the button you clicked is below them, so the commit pushes it down off-screen in a single jump. Your cursor sits where the button used to be.',
'with-anchor':
'Same coordinated commit, but useScrollAnchor pins the button before the layout change. The cards still flip together; the page scrolls to keep the button under your cursor as content grows above it.',
};
function PanelCard({
panel,
detail,
onChangeDetail,
}: {
panel: PanelName;
detail: Detail;
onChangeDetail: (next: Detail) => void;
}) {
const tuple = React.useMemo<[Detail, (next: Detail) => void]>(
() => [detail, onChangeDetail],
[detail, onChangeDetail],
);
const [lines, setLines] = React.useState<readonly string[]>(PANELS[panel][detail]);
const [visibleDetail, , extras] = useCoordinated<Detail, readonly string[]>(tuple, {
channelKey: 'anchor-pairing-demo',
peerId: `coord-${panel}`,
causesLayoutShift: () => true,
preload: (target, signal) => fetchLines(panel, target, signal),
// I/O-bound preload — show the card's loading badge while the
// fetch is in flight rather than holding it until commit.
animateDuringPreload: true,
onCommit: (_target, preloaded) => {
if (preloaded) {
setLines(preloaded);
}
},
ultimateTimeoutMs: 3000,
gracePeriodMs: 500,
});
return (
<div className={styles.card} data-loading={extras.isCoordinating || undefined}>
<div className={styles.cardHeader}>
<span className={styles.cardTitle}>{panel}</span>
<span className={styles.cardBadge} data-visible={extras.isCoordinating || undefined}>
Loading…
</span>
</div>
<p className={styles.cardMeta}>
Detail: <strong>{visibleDetail}</strong>
<span
className={styles.cardPending}
data-visible={extras.pendingValue !== visibleDetail || undefined}
>
→ {extras.pendingValue}
</span>
</p>
<ul className={styles.cardLines}>
{lines.map((line) => (
<li key={`${panel}-${line}`}>{line}</li>
))}
</ul>
</div>
);
}
function Stage({ mode }: { mode: Mode }) {
// Default to `summary`: going from a larger detail level toward
// `summary` shrinks the cards above the button, which shifts the
// page upward without pushing the button off-screen, so the
// demo's initial state is the "calmest" baseline.
const [detail, setDetail] = React.useState<Detail>('summary');
// Anchor the page on the button itself: when the cards above grow
// taller, the button stays at the same viewport offset under the
// user's cursor instead of being pushed off-screen.
const { containerRef, scrollContainerRef, anchorScroll } = useScrollAnchor<
HTMLDivElement,
HTMLDivElement
>();
const buttonRowRef = React.useRef<HTMLDivElement>(null);
// Long enough to cover the slowest coordinated commit
// (max(FETCH_DELAYS) ≈ 550 ms) plus a generous safety margin.
const ANCHOR_DURATION_MS = 1200;
const changeDetail = React.useCallback(
(next: Detail) => {
if (mode === 'with-anchor') {
anchorScroll(buttonRowRef.current, ANCHOR_DURATION_MS);
}
setDetail(next);
},
[mode, anchorScroll],
);
return (
<div className={styles.viewport} ref={scrollContainerRef}>
<div className={styles.intro}>
<span className={styles.scrollHint}>
Scroll this panel so the “Change detail” buttons sit near the bottom edge, then click{' '}
<code>verbose</code> or <code>summary</code>. The cards above will grow or shrink past the
viewport — watch where the button row ends up.
</span>
</div>
<div className={styles.cards} ref={containerRef}>
{PANEL_NAMES.map((panel) => (
<PanelCard key={panel} panel={panel} detail={detail} onChangeDetail={setDetail} />
))}
</div>
<div className={styles.buttonRow} ref={buttonRowRef}>
<span className={styles.buttonRowLabel}>Change detail:</span>
{DETAILS.map((option) => (
<button
key={option}
type="button"
onClick={() => changeDetail(option)}
className={
detail === option
? `${styles.detailButton} ${styles.detailButtonSelected}`
: styles.detailButton
}
>
{option}
</button>
))}
</div>
<div className={styles.outro}>
<p>
The button row above stays{' '}
{mode === 'with-anchor'
? 'pinned at the same viewport offset — the panel scrolls under it to absorb the layout change.'
: 'where the document flow puts it — if the cards above grow, it slides out of view below.'}
</p>
</div>
</div>
);
}
export function AnchorPairing() {
const [mode, setMode] = React.useState<Mode>('no-anchor');
return (
<div className={styles.root} data-mode={mode}>
<div className={styles.controls}>
<fieldset className={styles.modePicker}>
<legend className={styles.modeLegend}>Anchoring strategy</legend>
{(Object.keys(MODE_LABELS) as Mode[]).map((value) => (
<label
key={value}
className={styles.modeOption}
data-active={value === mode}
data-quality={MODE_QUALITY[value]}
>
<input
type="radio"
name="anchor-pairing-mode"
value={value}
checked={value === mode}
onChange={() => setMode(value)}
/>
<span className={styles.modeBadge}>{MODE_BADGES[value]}</span>
<span>{MODE_LABELS[value]}</span>
</label>
))}
</fieldset>
</div>
<p className={styles.hint} data-quality={MODE_QUALITY[mode]}>
{MODE_DESCRIPTIONS[mode]}
</p>
<Stage key={mode} mode={mode} />
</div>
);
}A page full of deferred content — code blocks that swap from plain fallback to highlighted output on idle, say — swaps in at staggered times. A coordinated change that fires mid-swap can only unify the peers that have hydrated so far; the rest settle in a second wave.
useCoordinated closes that gap automatically: a layout-shifting commit
waits until every registered layout-shift source has settled, so the first
page-wide change lands as a single unified update. A host declares its deferred
content with useCoordinatedLazy, passing true once it has reached its stable
post-hydration layout:
import { useCoordinatedLazy } from '@mui/internal-docs-infra/useCoordinated';
function DeferredBlock({ settled }: { settled: boolean }) {
// Registers on mount; releases on settle and on unmount, so a block that
// unmounts mid-swap can't hold the page's coordinated shifts hostage.
useCoordinatedLazy(settled);
// …
}The wait applies only to layout-shifting commits (causesLayoutShift(target) === true) —
lazy changes still commit on the spot, the consumer's preload still starts
immediately (only the commit waits), and when nothing has registered the gate
is a no-op. A safety timeout releases it even if a source never settles.
const [value, setValue, extras] = useCoordinated(underlying, options);value — the visible (committed) value. Lags the underlying value while a barrier is open.setValue — replacement for the underlying setter. Updates pendingValue immediately and announces the change to the channel.extras.pendingValue — the most recently announced target. Use this for optimistic toolbar UI.extras.isCoordinating — true while a coordination this peer can animate is in flight. Originators with the default animateDuringPreload: false hold this false until their preload settles, then flip it true for the rest of the barrier; with animateDuringPreload: true it flips synchronously on the originating setter call so loading UI appears the instant the user clicks. Receivers and barrier joiners flip synchronously regardless.extras.isWaitingForPeers — true once gracePeriodMs has elapsed with the barrier still open (only set on the originator).useCoordinatedCoordinate 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:
pendingValue updates synchronously to the requested targetpreload (per phase rules) and waits for
sibling peers (phase 1 only)setValue is called
and this hook’s visible value flips, so the swap is
consistent with the optional onCommit side-effectReceiver flow — when the underlying value changes from outside
(e.g. a storage event from another tab):
pendingValue updates to matchvalue returned by this hook is held at the
previous value until the barrier resolves, then flipsPass channelKey: null to disable coordination — the hook becomes
a transparent pass-through of the underlying tuple.
| Parameter | Type | Description |
|---|---|---|
| underlying | | |
| options | |
[TValue, React.Dispatch<React.SetStateAction<TValue>>, UseCoordinatedExtras<TValue>]Two preset wrappers compose useCoordinated with common underlying primitives:
useCoordinatedLocalStorageuseLocalStorageState + coordination in one call. Cross-tab sync
happens via the underlying storage events; the coordinator handles
within-tab peers (sibling demos on the same page) so their visible
value flips land in a single layout pass.
| Parameter | Type | Description |
|---|---|---|
| storageKey | | localStorage key, or |
| initializer | | Initial value, identical to
|
| options | | Coordination options. Pass |
[string | null, React.Dispatch<React.SetStateAction<string | null>>, UseCoordinatedExtras<string | null>]useCoordinatedPreferenceusePreference + useCoordinated in one call.
Cross-tab sync happens through the underlying localStorage /
storage event flow that usePreference already provides; the
coordinator handles within-tab peers so demos that share the same
preference key commit their visible flip together rather than
cascading layout shifts.
| Parameter | Type | Description |
|---|---|---|
| type | | Preference type, identical to |
| name | | Variant/transform name(s), identical to
|
| initializer | | Initial value or initializer, identical to
|
| options | | Coordination options. Pass |
[string | null, React.Dispatch<React.SetStateAction<string | null>>, UseCoordinatedExtras<string | null>]type UseCoordinatedExtras<TValue> = {
/**
* The most recently announced target value. Equals the committed
* `value` when no coordination is in flight. Useful for showing an
* optimistic preview UI (toolbar selection, etc.) that should react
* instantly to a click even when the visible content has a pending
* barrier.
*/
pendingValue: TValue;
/**
* `true` while a coordination is in flight that this peer can drive
* an animation off of. For receivers and barrier joiners that lands
* synchronously with the announce. For originators the flip is
* controlled by `animateDuringPreload`: with the default
* `animateDuringPreload: false`, `isCoordinating` stays `false`
* until the originator's `preload` settles, then flips `true` for
* the remainder of the barrier; with `animateDuringPreload: true`,
* it flips synchronously on the originating setter call so the
* animation overlaps with an I/O-bound preload. Use
* [`pendingValue`](#pendingvalue) to drive intent-based affordances (toolbar
* selection, etc.) that should react instantly to a click
* regardless of this flag. Surfaces as `data-coordinating` on
* consumers.
*/
isCoordinating: boolean;
/**
* `true` once the grace period has elapsed with the barrier still
* unresolved. Only set on the originating peer. Surface as a
* "waiting" affordance to the user.
*/
isWaitingForPeers: boolean;
}Options for useCoordinated. See coordinatePreference for
the underlying semantics; only React-specific behaviors are
documented here.
type UseCoordinatedOptions<TValue, TPreload> = {
/**
* Coordination scope. All peers (component instances) that share a
* `channelKey` participate in the same layout-shift barrier. Pass
* `null` to opt out of coordination entirely (the hook becomes a
* plain pass-through of the underlying `[value, setValue]`).
*/
channelKey: string | null;
/**
* Stable identifier for *this* peer within the channel. Defaults to
* a freshly generated id on mount. Override when stable cross-mount
* identity matters (e.g. for analytics / debugging).
*/
peerId?: string;
/**
* Return `true` when applying this target would visibly shift
* layout — the peer joins the channel-wide barrier so all such
* peers commit together. Return `false` for non-disruptive changes
* — the peer commits lazily on its own self-serial chain. See
* `coordinatePreference` for the full semantics.
*/
causesLayoutShift: (target: TValue) => boolean;
/**
* Optional async work to run before the barrier commits (for
* `causesLayoutShift === true`) or before a lazy commit (for
* `false`). The resolved value is handed to `onCommit`.
*/
preload?: (target: TValue, signal: AbortSignal) => TPreload | Promise<TPreload>;
/**
* Hook fired inside the coordinated commit, before the visible
* value flips. Useful for installing precomputed payloads into
* neighboring state. The visible `value` returned from this hook
* always lags `pendingValue` until coordination settles, so this
* runs *with* the value flip, not before it.
*
* Also fires once on first mount, with the initial preloaded
* payload, so consumers can install precomputed state on hydration
* without a separate code path.
*
* Under normal conditions `preloaded` is whatever this peer's
* `preload` resolved to. It may still be `undefined` when:
* - the barrier force-resolved at `ultimateTimeoutMs` (a sibling
* peer crashed / hung; accompanied by a console warning),
* - `preload` threw (logged via `console.error`, treated as a
* no-op so the rest of the channel still commits), or
* - `preload` explicitly returned `undefined`.
*
* Handlers should tolerate the undefined case and fall back to a
* synchronous render path rather than throwing.
*/
onCommit?: (target: TValue, preloaded?: TPreload) => void;
/** See . */
minWaitMs?: number;
/** See . */
multiPeerExtraMinWaitMs?: number;
/** See . */
lazyMinWaitMs?: number;
/** See . */
gracePeriodMs?: number;
/** See . */
ultimateTimeoutMs?: number;
/**
* Controls whether `isCoordinating` flips *during* the preload
* or *after* it. `pendingValue` (the user-facing "intent"
* signal) always flips synchronously regardless of this flag —
* toolbars and other affordances stay responsive on click.
*
* - `false` (default) — defer `isCoordinating` until the
* originator's `preload` settles. Use this when the preload
* is CPU-bound (parsing, syntax highlighting, layout
* measurement, etc.) and the consumer drives a visible
* animation off `isCoordinating`. Running the animation
* concurrently with the preload would steal main-thread time
* from the compositor and produce a janky transition; with
* the flip deferred the animation only starts once the heavy
* work is done.
* - `true` — flip `isCoordinating` synchronously on the
* originating setter call, so the animation runs in parallel
* with the preload. Use this when the preload is I/O-bound
* (network fetches, `localStorage` reads, etc.) so the
* animation and the I/O roundtrip overlap.
*
* The coordinator always yields to the browser (via
* `scheduler.yield()` when available, otherwise a `setTimeout`
* macrotask) before invoking `preload`, so even synchronous
* preloads settle one macrotask after the originating setter
* call. This lets the intermediate loading state paint before
* the (potentially CPU-bound) preload monopolizes the main
* thread. This flag has no effect when `preload` is omitted;
* the flip is synchronous either way.
*
* Only the originator's flip is affected. Sibling peers picked
* up via `notifySiblings` still observe the receiver flow's
* synchronous flip, because their `isCoordinating` is driven
* by the originator's broadcast rather than a local click.
*/
animateDuringPreload?: boolean;
/**
* Scheduling priority for lazy-path commits.
*
* - `'idle'` (default) — lazy-path commits are deferred via
* `requestIdleCallback` so the browser can yield to in-flight
* paints and input. Use when the lazy peer's commit itself is
* main-thread heavy (DOM reconciliation of a freshly
* transformed tree, etc.).
* - `'normal'` — the commit lands as soon as the preload
* resolves. Use for I/O-bound `preload`s where the commit is
* cheap and you want each peer's swap to surface immediately;
* otherwise idle scheduling can cluster commits together near
* the slowest peer's settle.
*
* Has no effect when this peer takes the barrier path — barrier
* commits are batched synchronously inside the barrier's resolve
* microtask regardless.
*/
lazyCommitPriority?: 'idle' | 'normal';
}The underlying coordinatePreference engine routes each announcement to one of two phases:
causesLayoutShift(target) === true. All peers on the channel that announce the same target join a shared barrier. Their preloads run concurrently, but commits all fire together once every peer (or the grace period / ultimate timeout) is ready. This is what keeps multi-panel layouts from "popping" one panel at a time.causesLayoutShift(target) === false. The peer joins a per-peer serial chain instead of the barrier. Its preload runs on an idle callback so it doesn't fight with the main thread, and it commits as soon as it's ready without holding up siblings.Peers already on the lazy path when a barrier opens are marked as skipped, so they never block the barrier.
All values in milliseconds. Defaults are tuned for the docs-infra demo viewer.
minWaitMs — minimum the barrier stays open from announceTime before it can resolve. Lets late peers join.multiPeerExtraMinWaitMs — added to minWaitMs when the channel has more than one peer.lazyMinWaitMs — minimum delay a lazy-path commit waits before firing (debounce for rapid changes).gracePeriodMs — when this elapses with the barrier still open, the originator's isWaitingForPeers flips to true.ultimateTimeoutMs — hard cap (default ~10s). After this the barrier force-resolves with a console warning; assume a peer crashed or unmounted mid-preload.setValue from this hook. Drives the barrier opening.storage event) without calling setValue themselves. The hook detects this and opens its own local barrier so visible flips still happen together inside the tab.The barrier will not block forever. After ultimateTimeoutMs (default ~10s) it force-resolves with a console warning of the form:
Barrier on channel '…' force-resolved after Xms; N waiter(s) still pending. A peer likely unmounted or crashed mid-preload.
When this happens, onCommit still fires on every surviving peer — but preloaded may be undefined even when you supplied a preload. Treat the warning as a real bug signal (a peer's preload is hanging or rejecting silently) and make sure every onCommit handler tolerates a missing preloaded value instead of throwing.
If the warning fires routinely under normal conditions, lower gracePeriodMs so isWaitingForPeers flips earlier and you can surface a transient indicator, then investigate which channel and peer keep timing out.
preload swallows the abort signalpreload is invoked with an AbortSignal that fires when a fresher announce supersedes the in-flight one. If your preload ignores the signal:
Always thread the signal through to fetch, downstream AbortControllers, and any worker calls, and check signal.aborted before doing significant post-await work. A superseded preload's eventual return value is simply discarded — only the freshest announce's onCommit fires — so you don't need a special "aborted" code path beyond letting the underlying work stop.
causesLayoutShift returns the wrong classificationcausesLayoutShift decides whether an announce joins the synchronized barrier path or the per-peer lazy path. A misclassification produces visible symptoms:
false when the swap does shift layout. The peer takes the lazy path and commits independently of the barrier, so a sibling demo on the page will flash to the new height a frame or two before the others, undoing the whole reason for using useCoordinated. Fix by returning true for the targets that resize, replace, or reflow visible content.true when the swap doesn't shift layout. Cheap, non-layout swaps get parked behind the barrier and wait for the slowest peer — UI feels sluggish for no reason. Fix by returning false so those swaps go through the lazy path.true join the barrier; peers that return false are marked skipped and commit immediately. If you expect peers to land together, make sure every peer evaluates the same target with the same predicate (e.g. read shared config rather than per-instance state).When in doubt, default to true (synchronized) for visible structural swaps and false only for purely cosmetic transitions where independent timing is acceptable.