206 lines
5.0 KiB
TypeScript
Executable File
206 lines
5.0 KiB
TypeScript
Executable File
"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,
|
|
};
|
|
}
|