evidence-binder/src/visual-guide/react-hooks.ts

99 lines
2.9 KiB
TypeScript
Raw Normal View History

/**
* React hooks for the rect registry.
*
* Components mount, get a ref to a DOM node, and ask the registry to
* track it via `useRegisterRect(kind, id, ref)`. Unmount/ref-change
* unregisters automatically.
*
* The registry itself lives behind a React context so multiple subtrees
* can share one registry (the overlay sees what every renderer publishes).
*/
import {
createContext,
useCallback,
useContext,
useEffect,
useSyncExternalStore,
type RefObject,
} from "react";
import {
createRectRegistry,
type RectKind,
type RectRegistry,
} from "./rect-registry";
import { attachRectChangePumps, type RectChangeObserverHandle } from "./events";
export interface RectRegistryContextValue {
readonly registry: RectRegistry;
readonly observer: RectChangeObserverHandle;
}
const RectRegistryContext = createContext<RectRegistryContextValue | null>(null);
/**
* Create an isolated registry + change pump pair for tests or app
* composition roots that wire their own provider.
*/
export function createRectRegistryContextValue(): RectRegistryContextValue {
const registry = createRectRegistry();
const observer = attachRectChangePumps(registry);
return { registry, observer };
}
export function useRectRegistryContext(): RectRegistryContextValue {
const ctx = useContext(RectRegistryContext);
if (!ctx) {
throw new Error(
"useRectRegistryContext must be used inside <RectRegistryProvider />",
);
}
return ctx;
}
export const RectRegistryProvider = RectRegistryContext.Provider;
/**
* Register a DOM ref's bounding rect with the registry.
*
* Re-runs when `kind`/`id`/`ref.current` change. The observer also starts
* watching the element for scroll/resize so the overlay can re-query
* without polling.
*/
export function useRegisterRect(
kind: RectKind,
id: string,
ref: RefObject<Element | null>,
): void {
const { registry, observer } = useRectRegistryContext();
useEffect(() => {
const el = ref.current;
if (!el) return;
const unregister = registry.register(kind, id, () => el.getBoundingClientRect());
const unobserve = observer.observe(el);
return () => {
unobserve();
unregister();
};
}, [kind, id, ref, registry, observer]);
}
/**
* Subscribe to registry change events from inside React. Returns a
* monotonically-increasing version number that bumps on every event, so
* `useMemo`/`useEffect` deps can include it to re-derive cached values.
*
* Implementation: leans on `registry.getVersion()` for the snapshot so
* `useSyncExternalStore` doesn't accumulate per-render subscribers.
*/
export function useRectRegistryVersion(): number {
const { registry } = useRectRegistryContext();
const subscribe = useCallback(
(callback: () => void) => registry.subscribe(callback),
[registry],
);
const getSnapshot = useCallback(() => registry.getVersion(), [registry]);
return useSyncExternalStore(subscribe, getSnapshot, () => 0);
}