import {
useEffect,
useSyncExternalStore,
type ReactNode,
type FC,
} from "react";
/**
* External store for the page's header title + actions. We use an external
* store (not React context) so that pages can inject ReactNode without
* subscribing to the header state themselves — subscribing would trigger a
* re-render on every update, which in turn creates new JSX, which updates
* the store again, which re-renders… an infinite loop.
*
* Only reads the store via useSyncExternalStore; pages only write.
*/
type Listener = () => void;
let currentTitle: ReactNode = null;
let currentActions: ReactNode = null;
const listeners = new Set();
function notify() {
for (const l of listeners) l();
}
function subscribe(l: Listener): () => void {
listeners.add(l);
return () => {
listeners.delete(l);
};
}
/** Consumed only by — returns the current title. */
export function useHeaderTitle(): ReactNode {
return useSyncExternalStore(
subscribe,
() => currentTitle,
() => currentTitle,
);
}
/** Consumed only by — returns the current actions slot. */
export function useHeaderActions(): ReactNode {
return useSyncExternalStore(
subscribe,
() => currentActions,
() => currentActions,
);
}
/**
* Provider is now a no-op wrapper for backwards compatibility — the state
* lives in the module-level store above. Kept as a component so callers of
* don't need to change.
*/
export const HeaderActionsProvider: FC<{ children: ReactNode }> = ({
children,
}) => <>{children}>;
/** Mount a custom title into the app header. Cleans up on unmount. */
export function useSetPageTitle(node: ReactNode) {
useEffect(() => {
currentTitle = node;
notify();
return () => {
currentTitle = null;
notify();
};
});
}
/** Mount ReactNode into the header's actions slot. Cleans up on unmount. */
export function useSetHeaderActions(node: ReactNode) {
useEffect(() => {
currentActions = node;
notify();
return () => {
currentActions = null;
notify();
};
});
}