import { useCallback, useEffect, useRef, useState } from "react"; import { loadConfig, type RuntimeConfig } from "./config"; /** useConfig resolves the runtime config (apiBase + rootDomain) once on mount. * Returns null until loaded; loadConfig itself never rejects (it falls back). */ export function useConfig(): RuntimeConfig | null { const [cfg, setCfg] = useState(null); useEffect(() => { let alive = true; loadConfig().then((c) => { if (alive) setCfg(c); }); return () => { alive = false; }; }, []); return cfg; } export interface AsyncState { data: T | null; error: unknown; loading: boolean; /** Re-run the async fn (e.g. after a mutation). */ reload: () => void; } /** useAsync runs an async producer on mount and on demand, guarding against * setState-after-unmount and out-of-order responses. */ export function useAsync(fn: () => Promise, deps: unknown[] = []): AsyncState { const [data, setData] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const seq = useRef(0); // eslint-disable-next-line react-hooks/exhaustive-deps const run = useCallback(fn, deps); const reload = useCallback(() => { const ticket = ++seq.current; setLoading(true); setError(null); run().then( (d) => { if (ticket === seq.current) { setData(d); setLoading(false); } }, (e) => { if (ticket === seq.current) { setError(e); setLoading(false); } }, ); }, [run]); useEffect(() => { reload(); return () => { seq.current++; }; }, [reload]); return { data, error, loading, reload }; } /** useUnsavedGuard asks the browser to confirm leaving the page (reload, tab * close, typing another URL) while `dirty` holds, so unsaved edits are not * dropped without a prompt. */ export function useUnsavedGuard(dirty: boolean): void { useEffect(() => { if (!dirty) return; const onBeforeUnload = (e: BeforeUnloadEvent) => { e.preventDefault(); // Older Chromium and Safari need returnValue set to show the prompt. e.returnValue = ""; }; window.addEventListener("beforeunload", onBeforeUnload); return () => window.removeEventListener("beforeunload", onBeforeUnload); }, [dirty]); }