Some checks failed
CI / test (push) Failing after 1m10s
Seven pages repeated the same load-on-mount + loading + try/finally +
reload-button skeleton, each re-implementing tab-active refresh, silent
polling, and (in two places) a hand-rolled requestId race guard. Extract two
composable hooks: useResource(fetcher, {tabPath, deps}) owning data/loading/
reload with a built-in race guard and auto tab-active refresh, and
usePolling(fn, ms, enabled) replacing the hand-written setInterval effects.
Migrate all seven pages onto them; Targets/Scenarios/ModelConfigs also gain a
uniform tab-active refresh they previously lacked. Verified via tsc --noEmit
and npm run build (no frontend test runner exists).
64 lines
2.0 KiB
TypeScript
64 lines
2.0 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
import { useOnTabActive } from './useOnTabActive'
|
|
|
|
interface UseResourceOptions {
|
|
/** Refetch whenever this keep-alive tab path becomes active again. */
|
|
tabPath?: string
|
|
/** Re-run the fetcher when any of these values change (like useEffect deps). */
|
|
deps?: readonly unknown[]
|
|
/** Fetch immediately on mount (default true). */
|
|
immediate?: boolean
|
|
}
|
|
|
|
interface UseResource<T> {
|
|
data: T | null
|
|
loading: boolean
|
|
/** Refetch. Pass `true` to skip the loading flag (silent poll refresh). */
|
|
reload: (silent?: boolean) => Promise<void>
|
|
}
|
|
|
|
/**
|
|
* Load-on-mount data with a loading flag, a manual/tab-active refetch, and a
|
|
* request-id race guard that drops stale responses (so a slow earlier fetch
|
|
* never overwrites a newer one). Errors propagate to the axios interceptor,
|
|
* matching every page's existing swallow-and-toast behaviour.
|
|
*
|
|
* `fetcher` returns the already-unwrapped value (call `.then(r => r.data)` or
|
|
* combine several requests with Promise.all before resolving).
|
|
*/
|
|
export function useResource<T>(
|
|
fetcher: () => Promise<T>,
|
|
options: UseResourceOptions = {},
|
|
): UseResource<T> {
|
|
const { tabPath, deps = [], immediate = true } = options
|
|
|
|
const [data, setData] = useState<T | null>(null)
|
|
const [loading, setLoading] = useState(immediate)
|
|
|
|
const fetcherRef = useRef(fetcher)
|
|
fetcherRef.current = fetcher
|
|
const requestId = useRef(0)
|
|
|
|
const reload = useCallback(async (silent = false) => {
|
|
const id = ++requestId.current
|
|
if (!silent) setLoading(true)
|
|
try {
|
|
const result = await fetcherRef.current()
|
|
if (id === requestId.current) setData(result)
|
|
} finally {
|
|
if (id === requestId.current && !silent) setLoading(false)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (immediate) void reload()
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, deps)
|
|
|
|
useOnTabActive(tabPath ?? '', () => {
|
|
if (tabPath) void reload()
|
|
})
|
|
|
|
return { data, loading, reload }
|
|
}
|