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:
@@ -14,6 +14,7 @@ Hooks that expose a `refresh()` callback must use a long-lived `AbortController`
|
||||
- Pass `controller.signal` to the API function.
|
||||
- In the cleanup effect, abort the controller when the hook unmounts.
|
||||
- After each `await`, check `signal.aborted` before updating state.
|
||||
- `refresh()` returns `TData | null` — `null` indicates the request was aborted. Callers must handle this case explicitly.
|
||||
|
||||
This prevents stale responses from overwriting newer results and avoids React state updates after unmount.
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -195,7 +195,7 @@ export function usePolledData<TResponse, TData>(
|
||||
if (!refetchOnWindowFocus) return;
|
||||
|
||||
const onFocus = (): void => {
|
||||
refreshRef.current();
|
||||
refreshRef.current?.();
|
||||
};
|
||||
|
||||
window.addEventListener("focus", onFocus);
|
||||
|
||||
Reference in New Issue
Block a user