diff options
| author | joonhoekim <26rote@gmail.com> | 2025-12-08 15:58:20 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-12-08 15:58:20 +0900 |
| commit | 137dc8abffcac7721890f320f183ab13eb30b790 (patch) | |
| tree | 78a2362cfadbfb1297ce0f86608256dc932e95cc /components/client-table-v3/use-client-table.ts | |
| parent | ad29c8d9dc5ce3f57d1e994e84603edcdb961c12 (diff) | |
| parent | d853ddc380bc03d968872e9ce53d7ea13a5304f8 (diff) | |
Merge branch 'table-v2' into dujinkim
Diffstat (limited to 'components/client-table-v3/use-client-table.ts')
| -rw-r--r-- | components/client-table-v3/use-client-table.ts | 283 |
1 files changed, 283 insertions, 0 deletions
diff --git a/components/client-table-v3/use-client-table.ts b/components/client-table-v3/use-client-table.ts new file mode 100644 index 00000000..87ce8a78 --- /dev/null +++ b/components/client-table-v3/use-client-table.ts @@ -0,0 +1,283 @@ +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<any> = (row, columnId, value, addMeta) => { + const itemRank = rankItem(row.getValue(columnId), value); + addMeta({ itemRank }); + return itemRank.passed; +}; + +// Simple debounce hook +function useDebounce<T>(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<TData, TValue> { + // Data Source + data?: TData[]; // For client mode + fetcher?: TableFetcher<TData>; // For server mode + fetchMode?: "client" | "server"; + + // Columns + columns: ColumnDef<TData, TValue>[]; + + // 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<TData, TValue = unknown>({ + data: initialData = [], + fetcher, + fetchMode = "client", + columns, + enableGrouping = false, + enablePagination = true, + enableRowSelection, + enableMultiRowSelection, + initialState, + onDataChange, + onError, + getRowId, +}: UseClientTableProps<TData, TValue>) { + // 1. State Definitions + const [sorting, setSorting] = React.useState<SortingState>(initialState?.sorting ?? []); + const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(initialState?.columnFilters ?? []); + const [globalFilter, setGlobalFilter] = React.useState<string>(initialState?.globalFilter ?? ""); + const [pagination, setPagination] = React.useState<PaginationState>( + initialState?.pagination ?? { pageIndex: 0, pageSize: 10 } + ); + const [grouping, setGrouping] = React.useState<GroupingState>(initialState?.grouping ?? []); + const [expanded, setExpanded] = React.useState<ExpandedState>(initialState?.expanded ?? {}); + const [rowSelection, setRowSelection] = React.useState<RowSelectionState>(initialState?.rowSelection ?? {}); + const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>(initialState?.columnVisibility ?? {}); + const [columnPinning, setColumnPinning] = React.useState<ColumnPinningState>( + initialState?.columnPinning ?? { left: [], right: [] } + ); + const [columnOrder, setColumnOrder] = React.useState<ColumnOrderState>( + initialState?.columnOrder ?? columns.map((c) => c.id || (c as any).accessorKey) as string[] + ); + + // 2. Data State + const [data, setData] = React.useState<TData[]>(initialData); + const [totalRows, setTotalRows] = React.useState<number>(initialData.length); + const [pageCount, setPageCount] = React.useState<number>(-1); + const [isLoading, setIsLoading] = React.useState<boolean>(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<any[]>([]); + 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, + }; +} + + |
