Files
DorkNet/DorkNet.Server/admin-ui/src/lib/useApi.ts
T
Alexa 4edf25b2d5 Import december-2020-12-18 from private repo (scrubbed 2026-05-27)
Targets Rec Room 2020.12.18 client.

Re-import after adding:
- DorkNet.ClientMod/dorknet-clientmod.json.template — config schema
  the launcher's manifest-driven patcher renders into UserData/.
- tools/build-patcher-zip.ps1 — produces the dorknet-clientpatch-{key}.zip
  Release asset the launcher fetches.
- .github/workflows/release.yml — auto-publishes both Release assets
  on v1-december-* tag push.

Scrubbed via maintainer-side scrub-for-public.ps1: same drop rules as
the prior import, plus build-patcher-zip.ps1 added to the tools/
allow-list so it survives the scrub.
2026-05-27 01:04:12 +03:00

48 lines
1.8 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react';
import { api, ApiError, type ApiOptions } from './api';
interface State<T> {
data: T | null;
loading: boolean;
error: string | null;
}
// Read-side hook for "fetch on mount, expose refresh + error state".
// Not a replacement for TanStack Query — we don't need its cache /
// dedupe / focus-refetch behavior for an admin tool used by 1-2 people.
// But every page does the same useState/useEffect/abort dance, so this
// collapses it to one line.
export function useApi<T = unknown>(path: string, opts: ApiOptions & { manual?: boolean } = {}) {
const [state, setState] = useState<State<T>>({ data: null, loading: !opts.manual, error: null });
const abortRef = useRef<AbortController | null>(null);
const { manual, ...rest } = opts;
// Stabilise opts across renders by capturing in a ref the caller can ignore.
// Anything that varies between renders (e.g. inline objects) is fine because
// we re-run on `path` change only.
const optsRef = useRef(rest);
optsRef.current = rest;
const refresh = useCallback(async () => {
abortRef.current?.abort();
const ctrl = new AbortController();
abortRef.current = ctrl;
setState(s => ({ ...s, loading: true, error: null }));
try {
const data = await api<T>(path, { ...optsRef.current, signal: ctrl.signal });
if (!ctrl.signal.aborted) setState({ data, loading: false, error: null });
} catch (e) {
if (ctrl.signal.aborted) return;
const msg = e instanceof ApiError ? e.message : (e as Error).message;
setState({ data: null, loading: false, error: msg });
}
}, [path]);
useEffect(() => {
if (manual) return;
refresh();
return () => { abortRef.current?.abort(); };
}, [refresh, manual]);
return { ...state, refresh };
}