<ChartContainer> mounts its own ChartInstance. Capture each one with a small helper inside the container that reads useChartInstance() and lifts it to parent state — refs won't trigger the rerender that lets the sync effect rebind.function RegisterChart({ onReady }) { const chart = useChartInstance(); useEffect(() => { onReady(chart); return () => onReady(null); }, [chart, onReady]); return null;} // Parent: state, not refs — so this component re-renders when// each chart mounts and the sync effect can rebind.const [priceChart, setPriceChart] = useState(null);const [volumeChart, setVolumeChart] = useState(null);const [rsiChart, setRsiChart] = useState(null);apply(src, dst). A broadcasting flag in the effect closure shorts cascade emits: when peer.setX inside the loop fires its own event, the receiver's handler sees the flag set and returns before iterating peers. Per-gesture cost is O(n) — n − 1 real applies plus n − 1 short-circuited invocations. No idempotency dependency; setVisibleRange / setCrosshair stay idempotent as defense in depth.function useBroadcast(charts, event, apply) { useEffect(() => { const ready = charts.filter(Boolean); let broadcasting = false; const handlers = ready.map((source) => { const onEvent = () => { if (broadcasting) return; broadcasting = true; try { for (const other of ready) { if (other !== source) apply(source, other); } } finally { broadcasting = false; } }; source.on(event, onEvent); return { source, onEvent }; }); return () => { for (const { source, onEvent } of handlers) { source.off(event, onEvent); } }; }, [...charts, event, apply]);}useBroadcast. apply functions live as module-level constants — stable identity, no useCallback dance, no listener re-binds on parent re-renders. Pan/zoom any chart and the rest follow; hover any pane and every pane's native <Crosshair> + <Tooltip> tracks the same time x. The { gesture: true } flag on setVisibleRange eases the synced panes on the same fast profile as a hand pan/zoom, so their Y axes re-fit in lockstep instead of crawling behind on the slower programmatic settle.// Module scope:const applyViewport = (src, dst) => { // gesture: true — ease in lockstep with the driving pan/zoom // instead of the slower programmatic (sticky-Y) settle. dst.setVisibleRange(src.getVisibleRange(), { gesture: true });}; const applyCrosshair = (src, dst) => { const pos = src.getCrosshairPosition(); dst.setCrosshair(pos ? { time: pos.time } : null);}; function useSyncViewport(charts) { useBroadcast(charts, 'viewportChange', applyViewport);} function useSyncCrosshair(charts) { useBroadcast(charts, 'crosshairMove', applyCrosshair);} // Parent:useSyncViewport(charts);useSyncCrosshair(charts);