import * as React from "react"; import { useReactTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel, getPaginationRowModel, getGroupedRowModel, getExpandedRowModel, getFacetedRowModel, getFacetedUniqueValues, getFacetedMinMaxValues, Table, ColumnDef, SortingState, ColumnFiltersState, PaginationState, VisibilityState, ColumnPinningState, ColumnOrderState, GroupingState, ExpandedState, RowSelectionState, OnChangeFn, FilterFn, } from "@tanstack/react-table"; import { rankItem } from "@tanstack/match-sorter-utils"; import { TableFetcher, FetcherResult } from "./types"; // --- Utils --- const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { const itemRank = rankItem(row.getValue(columnId), value); addMeta({ itemRank }); return itemRank.passed; }; // Simple debounce hook function useDebounce(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = React.useState(value); React.useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => clearTimeout(handler); }, [value, delay]); return debouncedValue; } // --- Props --- export interface UseClientTableProps { // Data Source data?: TData[]; // For client mode fetcher?: TableFetcher; // For server mode fetchMode?: "client" | "server"; // Columns columns: ColumnDef[]; // Options enableGrouping?: boolean; enablePagination?: boolean; enableRowSelection?: boolean | ((row: any) => boolean); enableMultiRowSelection?: boolean | ((row: any) => boolean); // Initial State (Optional overrides) initialState?: { pagination?: PaginationState; sorting?: SortingState; columnFilters?: ColumnFiltersState; globalFilter?: string; columnVisibility?: VisibilityState; columnPinning?: ColumnPinningState; columnOrder?: ColumnOrderState; grouping?: GroupingState; expanded?: ExpandedState; rowSelection?: RowSelectionState; }; // Callbacks onDataChange?: (data: TData[]) => void; onError?: (error: any) => void; // Custom Row ID getRowId?: (originalRow: TData, index: number, parent?: any) => string; } // --- Hook --- export function useClientTable({ data: initialData = [], fetcher, fetchMode = "client", columns, enableGrouping = false, enablePagination = true, enableRowSelection, enableMultiRowSelection, initialState, onDataChange, onError, getRowId, }: UseClientTableProps) { // 1. State Definitions const [sorting, setSorting] = React.useState(initialState?.sorting ?? []); const [columnFilters, setColumnFilters] = React.useState(initialState?.columnFilters ?? []); const [globalFilter, setGlobalFilter] = React.useState(initialState?.globalFilter ?? ""); const [pagination, setPagination] = React.useState( initialState?.pagination ?? { pageIndex: 0, pageSize: 10 } ); const [grouping, setGrouping] = React.useState(initialState?.grouping ?? []); const [expanded, setExpanded] = React.useState(initialState?.expanded ?? {}); const [rowSelection, setRowSelection] = React.useState(initialState?.rowSelection ?? {}); const [columnVisibility, setColumnVisibility] = React.useState(initialState?.columnVisibility ?? {}); const [columnPinning, setColumnPinning] = React.useState( initialState?.columnPinning ?? { left: [], right: [] } ); const [columnOrder, setColumnOrder] = React.useState( initialState?.columnOrder ?? columns.map((c) => c.id || (c as any).accessorKey) as string[] ); // 2. Data State const [data, setData] = React.useState(initialData); const [totalRows, setTotalRows] = React.useState(initialData.length); const [pageCount, setPageCount] = React.useState(-1); const [isLoading, setIsLoading] = React.useState(false); // Grouping specific data // In Pattern 2-B, the server returns "groups" instead of flat data. // We might need to store that separately or handle it within data if using TanStack's mix. // For now, let's assume the fetcher returns a flat array or we handle groups manually in the component. // But wait, client-virtual-table V2 handles groups by checking row.getIsGrouped(). // If fetchMode is server, and grouping is active, the server might return "groups". // The V2 implementation handled this by setting `groups` state in the consumer and switching rendering. // We want to encapsulate this. const [serverGroups, setServerGroups] = React.useState([]); const [isServerGrouped, setIsServerGrouped] = React.useState(false); const isServer = fetchMode === "server"; // Debounced states for fetching to avoid rapid-fire requests const debouncedGlobalFilter = useDebounce(globalFilter, 300); const debouncedColumnFilters = useDebounce(columnFilters, 300); // Pagination and Sorting don't need debounce usually, but grouping might. // 3. Data Fetching (Server Mode) const refresh = React.useCallback(async () => { if (!isServer || !fetcher) return; setIsLoading(true); try { const result = await fetcher({ pagination, sorting, columnFilters: debouncedColumnFilters, globalFilter: debouncedGlobalFilter, grouping, expanded, }); if (result.groups) { setServerGroups(result.groups); setIsServerGrouped(true); setData([]); // Clear flat data } else { setData(result.data); setTotalRows(result.totalRows); setPageCount(result.pageCount ?? -1); setServerGroups([]); setIsServerGrouped(false); if (onDataChange) onDataChange(result.data); } } catch (err) { console.error("Failed to fetch table data:", err); if (onError) onError(err); } finally { setIsLoading(false); } }, [ isServer, fetcher, pagination, sorting, debouncedColumnFilters, debouncedGlobalFilter, grouping, expanded, onDataChange, onError, ]); // Initial fetch and refetch on state change React.useEffect(() => { if (isServer) { refresh(); } }, [refresh, isServer]); // Update data when props change in Client Mode React.useEffect(() => { if (!isServer) { setData(initialData); setTotalRows(initialData.length); } }, [initialData, isServer]); // 4. TanStack Table Instance const table = useReactTable({ data, columns, state: { sorting, columnFilters, globalFilter, pagination, columnVisibility, columnPinning, columnOrder, rowSelection, grouping, expanded, }, // Server-side Flags manualPagination: isServer, manualSorting: isServer, manualFiltering: isServer, manualGrouping: isServer, // Counts pageCount: isServer ? pageCount : undefined, rowCount: isServer ? totalRows : undefined, // Handlers onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, onGlobalFilterChange: setGlobalFilter, onPaginationChange: setPagination, onColumnVisibilityChange: setColumnVisibility, onColumnPinningChange: setColumnPinning, onColumnOrderChange: setColumnOrder, onRowSelectionChange: setRowSelection, onGroupingChange: setGrouping, onExpandedChange: setExpanded, // Configs enableRowSelection, enableMultiRowSelection, enableGrouping, getCoreRowModel: getCoreRowModel(), // Conditional Models (Client vs Server) getFilteredRowModel: !isServer ? getFilteredRowModel() : undefined, getFacetedRowModel: !isServer ? getFacetedRowModel() : undefined, getFacetedUniqueValues: !isServer ? getFacetedUniqueValues() : undefined, getFacetedMinMaxValues: !isServer ? getFacetedMinMaxValues() : undefined, getSortedRowModel: !isServer ? getSortedRowModel() : undefined, getGroupedRowModel: (!isServer && enableGrouping) ? getGroupedRowModel() : undefined, getExpandedRowModel: (!isServer && enableGrouping) ? getExpandedRowModel() : undefined, getPaginationRowModel: (!isServer && enablePagination) ? getPaginationRowModel() : undefined, columnResizeMode: "onChange", filterFns: { fuzzy: fuzzyFilter, }, globalFilterFn: fuzzyFilter, getRowId, }); return { table, data, totalRows, isLoading, isServerGrouped, serverGroups, refresh, // State setters if needed manually setSorting, setColumnFilters, setPagination, setGlobalFilter, }; }