"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 { 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({ defaultLimit = 10, defaultSort = { id: "name", desc: false }, resourceName, fetchAction, staleTime = 30000, }: TableStateOptions) { const router = useRouter(); const searchParams = useSearchParams(); const queryClient = useQueryClient(); const [searchValue, setSearchValue] = useState(""); const [debouncedSearchValue, setDebouncedSearchValue] = useState(""); const [sorting, setSorting] = useState([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( () => ({ 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) => { 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) => { 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, }; }