Files
outline/app/hooks/usePaginatedRequest.ts
T
Tom Moor 0139b91b5d chore: Replace lodash with es-toolkit (#12281)
* chore: Replace lodash with es-toolkit

Migrate all direct lodash imports to es-toolkit/compat for a smaller,
faster, lodash-compatible utility library. Transitive lodash usage from
other packages remains unchanged.

* fix: Restore isPlainObject semantics in CanCan policy

The lodash migration aliased `isObject` to `lodash/isPlainObject` and
the codemod incorrectly mapped the local name to es-toolkit's `isObject`,
which also returns true for arrays and functions. This caused condition
objects in policy definitions to be skipped, breaking authorization
checks across the codebase.

* fix: Restore unicode-aware length counting in validators

es-toolkit/compat's size() returns string.length, while lodash's _.size()
counts unicode code points. Switch to [...value].length to preserve the
previous behavior so multi-byte characters like emoji count as one.
2026-05-06 21:03:47 -04:00

107 lines
2.6 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,
})
);
}
}, [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,
})
);
}, [requestFn]);
return { data, next, loading, error, page, offset, end };
}