satupeta-main/app/(modules)/admin/_hooks/use-table-state.tsx

206 lines
5.0 KiB
TypeScript
Raw Normal View History

2026-01-27 02:31:12 +00:00
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { SortingState } from "@tanstack/react-table";
import { useQuery, useQueryClient } from "@tanstack/react-query";
interface TableStateOptions<T> {
defaultLimit?: number;
defaultSort?: { id: string; desc: boolean };
resourceName: string;
fetchAction: (params: QueryParams) => Promise<{
items: T[];
total: number;
has_more: boolean;
}>;
staleTime?: number;
}
export interface QueryParams {
limit: number;
offset: number;
search?: string;
filter?: string[];
sort: string;
}
export function useTableState<T>({
defaultLimit = 10,
defaultSort = { id: "name", desc: false },
resourceName,
fetchAction,
staleTime = 30000,
}: TableStateOptions<T>) {
const router = useRouter();
const searchParams = useSearchParams();
const queryClient = useQueryClient();
const [searchValue, setSearchValue] = useState("");
const [debouncedSearchValue, setDebouncedSearchValue] = useState("");
const [sorting, setSorting] = useState<SortingState>([defaultSort]);
const { limit, offset, search, sortBy, sortOrder, filterParams } = useMemo(
() => ({
limit: Number(searchParams.get("limit") || defaultLimit),
offset: Number(searchParams.get("offset") || 0),
search: searchParams.get("search") || "",
sortBy: searchParams.get("sortBy") || defaultSort.id,
sortOrder:
searchParams.get("sortOrder") || (defaultSort.desc ? "desc" : "asc"),
filterParams: searchParams.getAll("filter").filter(Boolean),
}),
[searchParams, defaultLimit, defaultSort.id, defaultSort.desc]
);
useEffect(() => {
setSearchValue(search);
}, [search]);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedSearchValue(searchValue);
}, 500);
return () => clearTimeout(handler);
}, [searchValue]);
const queryParams = useMemo<QueryParams>(
() => ({
limit,
offset,
search: debouncedSearchValue || undefined,
filter: filterParams.length > 0 ? filterParams : undefined,
sort: `${sortBy}:${sortOrder}`,
}),
[limit, offset, debouncedSearchValue, filterParams, sortBy, sortOrder]
);
const queryKey = [
resourceName,
limit,
offset,
debouncedSearchValue,
filterParams.join("|"),
sortBy,
sortOrder,
];
const {
data = { items: [], total: 0, has_more: false },
isLoading,
isError,
refetch,
} = useQuery({
queryKey,
queryFn: () => fetchAction(queryParams),
staleTime,
retry: 1,
});
useEffect(() => {
if (data.has_more) {
const nextOffset = offset + limit;
queryClient.prefetchQuery({
queryKey: [
resourceName,
limit,
nextOffset,
debouncedSearchValue,
filterParams.join("|"),
sortBy,
sortOrder,
],
queryFn: () =>
fetchAction({
...queryParams,
offset: nextOffset,
}),
staleTime,
});
}
}, [
data.has_more,
limit,
offset,
debouncedSearchValue,
filterParams,
queryClient,
queryParams,
resourceName,
sortBy,
sortOrder,
fetchAction,
staleTime,
]);
const updateSearchParams = useCallback(
(params: Record<string, string>) => {
const newParams = new URLSearchParams(searchParams.toString());
Object.entries(params).forEach(([key, value]) => {
if (value) {
newParams.set(key, value);
} else {
newParams.delete(key);
}
});
router.push(`?${newParams.toString()}`);
},
[router, searchParams]
);
const updateSortingParams = useCallback(
(sorting: SortingState) => {
const newParams = new URLSearchParams(searchParams.toString());
if (sorting.length === 0) {
newParams.delete("sortBy");
newParams.delete("sortOrder");
} else {
newParams.set("sortBy", sorting[0].id);
newParams.set("sortOrder", sorting[0].desc ? "desc" : "asc");
}
router.push(`?${newParams.toString()}`);
},
[router, searchParams]
);
const handleSearchInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchValue(e.target.value);
};
const handlePaginationChange = useCallback(
({ pageIndex, pageSize }: { pageIndex: number; pageSize: number }) => {
const newOffset = pageIndex * pageSize;
const params = {
offset: pageSize !== limit ? "0" : newOffset.toString(),
limit: pageSize.toString(),
};
updateSearchParams(params);
},
[updateSearchParams, limit]
);
const pageIndex = Math.floor(offset / limit);
const pageCount = Math.ceil(data.total / limit);
return {
data: data.items,
total: data.total,
isLoading,
isError,
refetch,
searchValue,
sorting,
handleSearchInputChange,
handlePaginationChange,
updateSortingParams,
pageIndex,
pageCount,
limit,
setSorting,
};
}