57 lines
1.7 KiB
TypeScript
57 lines
1.7 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
|
|
type Snapshot<T> =
|
|
| { key: string; status: 'ready'; data: T }
|
|
| { key: string; status: 'error'; data: null };
|
|
|
|
export function useResource<T>(
|
|
url: string | null,
|
|
revision: number,
|
|
refresh = false
|
|
) {
|
|
const [snapshot, setSnapshot] = useState<Snapshot<T> | null>(null);
|
|
const key = `${url}:${revision}`;
|
|
|
|
useEffect(() => {
|
|
if (!url) return;
|
|
const controller = new AbortController();
|
|
let fetching = false;
|
|
async function load() {
|
|
if (fetching || controller.signal.aborted) return;
|
|
fetching = true;
|
|
try {
|
|
const response = await fetch(url!, {
|
|
signal: controller.signal,
|
|
cache: 'no-store',
|
|
});
|
|
if (!response.ok) throw new Error('Resource unavailable');
|
|
const data = (await response.json()) as T;
|
|
if (!controller.signal.aborted)
|
|
setSnapshot({ key, status: 'ready', data });
|
|
} catch {
|
|
if (!controller.signal.aborted)
|
|
setSnapshot({ key, status: 'error', data: null });
|
|
} finally {
|
|
fetching = false;
|
|
}
|
|
}
|
|
void load();
|
|
const refreshVisible = () => {
|
|
if (document.visibilityState === 'visible') void load();
|
|
};
|
|
const interval = refresh
|
|
? window.setInterval(refreshVisible, 30000)
|
|
: undefined;
|
|
if (refresh) document.addEventListener('visibilitychange', refreshVisible);
|
|
return () => {
|
|
controller.abort();
|
|
window.clearInterval(interval);
|
|
document.removeEventListener('visibilitychange', refreshVisible);
|
|
};
|
|
}, [url, key, refresh]);
|
|
|
|
if (!url) return { status: 'idle' as const, data: null };
|
|
if (snapshot?.key !== key) return { status: 'loading' as const, data: null };
|
|
return snapshot;
|
|
}
|