MuSProt / frontend /src /lib /hooks.ts
WinslowFan's picture
Claude Opus 5.5
Precompute inner graphs and drop duplicate edge rows
a8d1387
Raw History Blame Contribute Delete
2.57 kB
import { useEffect, useRef, useState } from 'react';
export interface AsyncState<T> {
data: T | null;
error: Error | null;
loading: boolean;
}
const cache = new Map<string, unknown>();
/**
* Fetch data keyed by `key`; aborts stale requests and memoises successful
* responses for the session so back/forward navigation is instant.
*/
export function useApi<T>(key: string | null, fetcher: (signal: AbortSignal) => Promise<T>): AsyncState<T> {
const cached = key ? (cache.get(key) as T | undefined) : undefined;
const [state, setState] = useState<AsyncState<T>>({
data: cached ?? null,
error: null,
loading: key !== null && cached === undefined,
});
const fetcherRef = useRef(fetcher);
fetcherRef.current = fetcher;
useEffect(() => {
if (key === null) {
setState({ data: null, error: null, loading: false });
return;
}
const hit = cache.get(key) as T | undefined;
if (hit !== undefined) {
setState({ data: hit, error: null, loading: false });
return;
}
const controller = new AbortController();
setState((s) => ({ data: s.data, error: null, loading: true }));
fetcherRef
.current(controller.signal)
.then((data) => {
cache.set(key, data);
if (!controller.signal.aborted) setState({ data, error: null, loading: false });
})
.catch((error: Error) => {
if (!controller.signal.aborted) setState({ data: null, error, loading: false });
});
return () => controller.abort();
}, [key]);
return state;
}
/** Warm the useApi cache for a key (e.g. the next thing a user is likely to open). */
export function prefetch<T>(key: string, fetcher: () => Promise<T>): void {
if (cache.has(key)) return;
fetcher()
.then((data) => cache.set(key, data))
.catch(() => {
/* best effort */
});
}
/** Resolve a best-effort promise (e.g. an RCSB title) into state. */
export function usePromise<T>(factory: () => Promise<T> | null, deps: unknown[]): T | null {
const [value, setValue] = useState<T | null>(null);
useEffect(() => {
let alive = true;
setValue(null);
const p = factory();
p?.then((v) => alive && setValue(v));
return () => {
alive = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return value;
}
export function useDebounced<T>(value: T, ms: number): T {
const [v, setV] = useState(value);
useEffect(() => {
const t = setTimeout(() => setV(value), ms);
return () => clearTimeout(t);
}, [value, ms]);
return v;
}