Files
outline/app/hooks/useTableRequest.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

93 lines
2.3 KiB
TypeScript

import type { ColumnSort } from "@tanstack/react-table";
import { orderBy } from "es-toolkit/compat";
import { useState, useRef, useCallback, useEffect } from "react";
import type { FetchPageParams, PaginatedResponse } from "~/stores/base/Store";
import { PAGINATION_SYMBOL } from "~/stores/base/Store";
import useRequest from "./useRequest";
const INITIAL_OFFSET = 0;
const PAGE_SIZE = 25;
type Props<T> = {
data: T[];
sort: ColumnSort;
reqFn: (params: FetchPageParams) => Promise<PaginatedResponse<T>>;
reqParams: Omit<FetchPageParams, "offset" | "limit">;
};
type Response<T> = {
data: T[] | undefined;
error: unknown;
loading: boolean;
next: (() => void) | undefined;
};
export function useTableRequest<T extends { id: string }>({
data,
sort,
reqFn,
reqParams,
}: Props<T>): Response<T> {
const [total, setTotal] = useState<number>();
const [offset, setOffset] = useState({ value: INITIAL_OFFSET });
const prevParamsRef = useRef(reqParams);
const sortRef = useRef<ColumnSort>(sort);
const fetchPage = useCallback(
() => reqFn({ ...reqParams, offset: offset.value, limit: PAGE_SIZE }),
[reqFn, reqParams, offset]
);
const { request, loading, error } = useRequest(fetchPage);
const nextPage = useCallback(
() =>
setOffset((prev) => ({
value: prev.value + PAGE_SIZE,
})),
[]
);
const sortedData = data
? orderBy(data, sortRef.current.id, sortRef.current.desc ? "desc" : "asc")
: undefined;
const next =
!loading && total && sortedData && sortedData.length < total
? nextPage
: undefined;
useEffect(() => {
if (prevParamsRef.current !== reqParams) {
prevParamsRef.current = reqParams;
setOffset({ value: INITIAL_OFFSET });
return;
}
let ignore = false;
const handleRequest = async () => {
const response = await request();
if (!response || ignore) {
return;
}
sortRef.current = sort; // Change sort once we receive a response from server - avoids flicker with stale data.
setTotal(response[PAGINATION_SYMBOL]?.total);
};
void handleRequest();
return () => {
ignore = true;
};
}, [sort, reqParams, offset, request]);
return {
data: sortedData,
error,
loading,
next,
};
}