One hook for async data
One useQuery hook returns data, error, loading and refetch.
From frontend-patterns, “Async Data Fetching Hook”.
Retry after a failure
Tick Fail and refetch both. Untick, then press Retry.
Ad-hoc fetchBad
loading: falseuseQueryGood
loading: falseexport function useQuery<T>(
key: string,
fetcher: () => Promise<T>,
options?: UseQueryOptions<T>
) {
const [data, setData] = useState<T | null>(null)
const [error, setError] = useState<Error | null>(null)
const [loading, setLoading] = useState(false)
const refetch = useCallback(async () => {
setLoading(true)
setError(null)
try {
const result = await fetcher()
setData(result)
options?.onSuccess?.(result)
} catch (err) {
setError(err as Error)
options?.onError?.(err as Error)
} finally {
setLoading(false)
}
}, [fetcher, options])
useEffect(() => {
if (options?.enabled !== false) refetch()
}, [key, refetch, options?.enabled])
return { data, error, loading, refetch }
}
Wait for inputs with enabled
Press Sign in, then Sign out.
userId: null
No
enabledBaduseQuery(key, fetcher)enabled: id !== nullGood
useQuery(key, fetcher, options)const fetchWatchlist = useCallback(
() => api.get(`/users/${userId}/watchlist`),
[userId]
)
const options = useMemo(() => ({ enabled: userId !== null }), [userId])
const { data, loading, error } = useQuery(`watchlist:${userId}`, fetchWatchlist, options)
Stable inputs, stable requests
Press Mount both and compare the request counts.
Inline fetcherBad
useQuery("markets", () => get(), { onSuccess })MemoizedGood
useCallback + useMemoconst fetchMarkets = useCallback(() => api.get("/markets"), [])
const options = useMemo(() => ({
onSuccess: (markets) => log(`Fetched ${markets.length} markets`),
onError: (err) => log(`Failed: ${err.status}`),
}), [])
const { data, error, loading, refetch } = useQuery("markets", fetchMarkets, options)
Why it matters, pros and cons
Why it matters
Every fetch has four states. Written once, every screen handles them the same.
Pros
finallyalways ends the spinner.- Retry never shows a stale error.
enabledwaits for missing inputs.
Cons
- Inline fetchers or options refetch in a loop.
- No cache, dedupe or cancellation.
- Real apps: Server Components, TanStack Query or SWR.