Refactor data fetching hooks, add page size lint test

- Simplify useFetchData: remove unused URL building logic
- Add usePolledData initial implementation
- Add router page_size param validation test
- Update API reference docs
- Clean up tasks doc
This commit is contained in:
2026-05-04 06:48:24 +02:00
parent 0a3f9c6c16
commit 69e1726045
7 changed files with 134 additions and 93 deletions

View File

@@ -72,8 +72,8 @@ export interface UseFetchDataResult<TData> {
loading: boolean;
/** Typed error or null. Check `error?.type` to handle specific failure modes. */
error: FetchError | null;
/** Trigger a fresh fetch. Cancels any in-flight request first. */
refresh: () => void;
/** Trigger a fresh fetch. Cancels any in-flight request first. Returns null if aborted, otherwise the selected data. */
refresh: () => Promise<TData | null>;
}
/**
@@ -107,7 +107,7 @@ export function useFetchData<TResponse, TData>(
/** Unique ID for this instance, used to track its subscription to deduplicated requests. */
const subscriberIdRef = useRef<string | null>(null);
const refresh = useCallback((): void => {
const refresh = useCallback(async (): Promise<TData | null> => {
// Abort any previous request from this hook instance
abortRef.current?.abort();
localControllerRef.current = new AbortController();
@@ -165,31 +165,41 @@ export function useFetchData<TResponse, TData>(
.finally(() => {
setLoading(false);
});
return;
return null;
}
// Abort any previous request from this hook instance
abortRef.current?.abort();
localControllerRef.current = new AbortController();
abortRef.current = localControllerRef.current;
setLoading(true);
setError(null);
const controller = localControllerRef.current;
const responsePromise = fetcher(controller.signal)
// Raw promise for deduplication storage - stores BEFORE transformation
const rawPromise = fetcher(controller.signal).catch((err: unknown) => {
if (controller.signal.aborted) throw err;
handleFetchError(err, setError, errorMessage);
throw err;
});
// Store in-flight BEFORE transformation for correct subscriber type
if (requestKey) {
inFlightRequests.set(requestKey, {
promise: rawPromise as Promise<TResponse>,
controller,
subscribers: new Map(),
initiatorDone: false,
});
}
// Transformed promise for return value - applies selector and returns TData | null
const responsePromise = rawPromise
.then((response) => {
if (controller.signal.aborted) return response;
setData(selector(response));
if (controller.signal.aborted) return null;
const data = selector(response);
setData(data);
if (onSuccess) {
onSuccess(response);
}
return response;
})
.catch((err: unknown) => {
if (controller.signal.aborted) throw err;
handleFetchError(err, setError, errorMessage);
throw err;
return data;
})
.finally(() => {
if (!controller.signal.aborted) {
@@ -200,15 +210,7 @@ export function useFetchData<TResponse, TData>(
}
});
// Store in-flight request for deduplication
if (requestKey) {
inFlightRequests.set(requestKey, {
promise: responsePromise,
controller,
subscribers: new Map(),
initiatorDone: false,
});
}
return responsePromise;
}, [fetcher, selector, errorMessage, onSuccess, requestKey]);
useEffect(() => {