summaryrefslogtreecommitdiff
path: root/components/client-table-v3/use-client-table.ts
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-12-08 12:08:00 +0900
committerjoonhoekim <26rote@gmail.com>2025-12-08 12:08:00 +0900
commit71c0ba1f01b98770ec2c60cdb935ffb36c1830a9 (patch)
tree01b5dab4888f865d605c46d41896a2432e53015f /components/client-table-v3/use-client-table.ts
parent298e166eebeba3276403f14f9b9b1e3f8f3d735e (diff)
(김준회) 테이블 커스텀 훅 버전 생성
Diffstat (limited to 'components/client-table-v3/use-client-table.ts')
-rw-r--r--components/client-table-v3/use-client-table.ts283
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,
+ };
+}
+
+