AgentEvalTool/frontend/web/src/hooks/useResource.ts
sinohqb 050c674ee2
Some checks failed
CI / test (push) Failing after 1m10s
refactor(frontend): extract useResource/usePolling shared hooks
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).
2026-07-31 11:01:24 +08:00

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 }
}