Files
outline/app/hooks/usePaginatedRequest.ts
T
Tom MoorandGitHub 08a3b64295 chore: Exhaustive deps warning -> error (#13124)
* chore: Exhaustive deps turned to error

* Refactor useDragResize

* Refactor Lightbox to explicit state machine

* feedback

* ci
2026-07-25 10:43:49 -04:00

113 lines
3.0 KiB
TypeScript

import { uniqBy } from "es-toolkit/compat";
import { useState, useEffect, useCallback } from "react";
import type { PaginationParams } from "~/types";
import useRequest from "./useRequest";
type RequestResponse<T> = {
/** The return value of the paginated request function. */
data: T[] | undefined;
/** The request error, if any. */
error: unknown;
/** Whether the request is currently in progress. */
loading: boolean;
/** Function to trigger next page request. */
next: () => void;
/** Page number */
page: number;
/** Offset */
offset: number;
/** Marks the end of pagination */
end: boolean;
};
const INITIAL_OFFSET = 0;
const DEFAULT_LIMIT = 10;
/**
* A hook to make paginated API request and track its state within a component.
*
* @param requestFn The function to call to make the request, it should return a promise.
* @param params Pagination params(limit, offset etc) to be passed to requestFn.
* @returns
*/
export default function usePaginatedRequest<T = unknown>(
requestFn: (params?: PaginationParams) => Promise<T[]>,
params: PaginationParams = {}
): RequestResponse<T> {
const [data, setData] = useState<T[]>();
const [offset, setOffset] = useState(INITIAL_OFFSET);
const [page, setPage] = useState(0);
const [end, setEnd] = useState(false);
const displayLimit = params.limit || DEFAULT_LIMIT;
const fetchLimit = displayLimit + 1;
const [paginatedReq, setPaginatedReq] = useState(
() => () =>
requestFn({
...params,
offset: 0,
limit: fetchLimit,
})
);
const {
data: response,
error,
loading,
request,
} = useRequest<T[]>(paginatedReq);
useEffect(() => {
void request();
}, [request]);
useEffect(() => {
if (response && !loading) {
setData((prev) =>
uniqBy((prev ?? []).concat(response.slice(0, displayLimit)), "id")
);
setPage((prev) => prev + 1);
setEnd(response.length <= displayLimit);
}
}, [response, displayLimit, loading]);
useEffect(() => {
if (offset) {
setPaginatedReq(
() => () =>
requestFn({
...params,
offset,
limit: fetchLimit,
})
);
}
// `params` is intentionally omitted, callers pass an object literal which
// changes identity on every render.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [offset, fetchLimit, requestFn]);
const next = useCallback(() => {
setOffset((prev) => prev + displayLimit);
}, [displayLimit]);
useEffect(() => {
setEnd(false);
setData(undefined);
setPage(0);
setOffset(0);
setPaginatedReq(
() => () =>
requestFn({
...params,
offset: 0,
limit: fetchLimit,
})
);
// `params` and `fetchLimit` are intentionally omitted, resetting pagination
// should only be driven by the request function changing.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [requestFn]);
return { data, next, loading, error, page, offset, end };
}