Version: 5.xx.xx
Source CodeTanStack Table
Refine provides an integration package for TanStack Table library. This package enables you to manage your tables in a headless manner. This adapter supports all of the features of both TanStack Table and Refine's useTable hook (sorting, filtering pagination etc). Simply, you can use any of the TanStack Table examples as-is by copying and pasting them into your project.
Installation
Install the @refinedev/react-table library.
- npm
- pnpm
- yarn
npm i @refinedev/react-table
pnpm add @refinedev/react-table
yarn add @refinedev/react-table
Usage
Let's see how to display a table with useTable hook.
We provide implementation examples for the Mantine and Chakra UI. If you using a different ui library, you can use the headless example as a starting point.
- Headless
- MantineTanStack Table
- Chakra UITanStack Table
Headless
Code Example
// file: /App.tsx
import React from "react";
import { Refine } from "@refinedev/core";
import dataProvider from "@refinedev/simple-rest";
import { ProductTable } from "./product-table.tsx";
const API_URL = "https://api.fake-rest.refine.dev";
export default function App() {
return (
<Refine dataProvider={dataProvider(API_URL)}>
<ProductTable />
</Refine>
);
}// file: /product-table.tsx
import React from "react";
import { useTable } from "@refinedev/react-table";
import { ColumnDef, flexRender } from "@tanstack/react-table";
export const ProductTable: React.FC = () => {
const columns = React.useMemo<ColumnDef<IProduct>[]>(
() => [
{
id: "id",
header: "ID",
accessorKey: "id",
meta: {
filterOperator: "eq",
},
},
{
id: "name",
header: "Name",
accessorKey: "name",
meta: {
filterOperator: "contains",
},
},
{
id: "price",
header: "Price",
accessorKey: "price",
meta: {
filterOperator: "eq",
},
},
],
[],
);
const {
getHeaderGroups,
getRowModel,
getState,
setPageIndex,
getCanPreviousPage,
getPageCount,
getCanNextPage,
nextPage,
previousPage,
setPageSize,
} = useTable<IProduct>({
refineCoreProps: {
resource: "products",
},
columns,
});
return (
<div>
<h1>Products</h1>
<table>
<thead>
{getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<th key={header.id}>
{header.isPlaceholder ? null : (
<>
<div
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(
header.column.columnDef
.header,
header.getContext(),
)}
{{
asc: " 🔼",
desc: " 🔽",
}[
header.column.getIsSorted() as string
] ?? " ↕️"}
</div>
</>
)}
{header.column.getCanFilter() ? (
<div>
<input
value={
(header.column.getFilterValue() as string) ??
""
}
onChange={(e) =>
header.column.setFilterValue(
e.target.value,
)
}
/>
</div>
) : null}
</th>
);
})}
</tr>
))}
</thead>
<tbody>
{getRowModel().rows.map((row) => {
return (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => {
return (
<td key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
<div>
<button
onClick={() => setPageIndex(0)}
disabled={!getCanPreviousPage()}
>
{"<<"}
</button>
<button
onClick={() => previousPage()}
disabled={!getCanPreviousPage()}
>
{"<"}
</button>
<button onClick={() => nextPage()} disabled={!getCanNextPage()}>
{">"}
</button>
<button
onClick={() => setPageIndex(getPageCount() - 1)}
disabled={!getCanNextPage()}
>
{">>"}
</button>
<span>
Page
<strong>
{getState().pagination.pageIndex + 1} of{" "}
{getPageCount()}
</strong>
</span>
<span>
| Go to page:
<input
type="number"
defaultValue={getState().pagination.pageIndex + 1}
onChange={(e) => {
const page = e.target.value
? Number(e.target.value) - 1
: 0;
setPageIndex(page);
}}
/>
</span>{" "}
<select
value={getState().pagination.pageSize}
onChange={(e) => {
setPageSize(Number(e.target.value));
}}
>
{[10, 20, 30, 40, 50].map((pageSize) => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
</div>
);
};
interface IProduct {
id: number;
name: string;
price: string;
}MantineTanStack Table
Code Example
// file: /App.tsx
import React from "react";
import { Refine } from "@refinedev/core";
import dataProvider from "@refinedev/simple-rest";
import { MantineProvider, Global } from "@mantine/core";
import { ProductTable } from "./product-table.tsx";
const API_URL = "https://api.fake-rest.refine.dev";
export default function App() {
return (
<MantineProvider
withNormalizeCSS
withGlobalStyles
>
<Global styles={{ body: { WebkitFontSmoothing: "auto" } }} />
<Refine dataProvider={dataProvider(API_URL)}>
<ProductTable />
</Refine>
</MantineProvider>
);
}// file: /product-table.tsx
import React from "react";
import { useTable } from "@refinedev/react-table";
import { ColumnDef, flexRender } from "@tanstack/react-table";
import { Box, Group, Table, Pagination } from "@mantine/core";
import { ColumnSorter } from "./column-sorter.tsx";
import { ColumnFilter } from "./column-filter.tsx";
export const ProductTable: React.FC = () => {
const columns = React.useMemo<ColumnDef<IProduct>[]>(
() => [
{
id: "id",
header: "ID",
accessorKey: "id",
meta: {
filterOperator: "eq",
},
},
{
id: "name",
header: "Name",
accessorKey: "name",
meta: {
filterOperator: "contains",
},
},
{
id: "price",
header: "Price",
accessorKey: "price",
meta: {
filterOperator: "eq",
},
},
],
[],
);
const {
getHeaderGroups,
getRowModel,
refineCore: { setCurrentPage, pageCount, currentPage },
} = useTable({
refineCoreProps: {
resource: "products",
},
columns,
});
return (
<div style={{ padding: "4px" }}>
<h2>Products</h2>
<Table highlightOnHover>
<thead>
{getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<th key={header.id}>
{!header.isPlaceholder && (
<Group spacing="xs" noWrap>
<Box>
{flexRender(
header.column.columnDef
.header,
header.getContext(),
)}
</Box>
<Group spacing="xs" noWrap>
<ColumnSorter
column={header.column}
/>
<ColumnFilter
column={header.column}
/>
</Group>
</Group>
)}
</th>
);
})}
</tr>
))}
</thead>
<tbody>
{getRowModel().rows.map((row) => {
return (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => {
return (
<td key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</td>
);
})}
</tr>
);
})}
</tbody>
</Table>
<br />
<Pagination
position="right"
total={pageCount}
page={currentPage}
onChange={setCurrentPage}
/>
</div>
);
};
interface IProduct {
id: number;
name: string;
price: string;
}// file: /column-sorter.tsx
import { ActionIcon } from "@mantine/core";
import { IconChevronDown, IconSelector, IconChevronUp } from "@tabler/icons-react";
export interface ColumnButtonProps {
column: Column<any, any>; // eslint-disable-line
}
export const ColumnSorter: React.FC<ColumnButtonProps> = ({ column }) => {
if (!column.getCanSort()) {
return null;
}
const sorted = column.getIsSorted();
return (
<ActionIcon
size="xs"
onClick={column.getToggleSortingHandler()}
style={{
transition: "transform 0.25s",
transform: `rotate(${sorted === "asc" ? "180" : "0"}deg)`,
}}
variant={sorted ? "light" : "transparent"}
color={sorted ? "primary" : "gray"}
>
{!sorted && <IconSelector size={18} />}
{sorted === "asc" && <IconChevronDown size={18} />}
{sorted === "desc" && <IconChevronUp size={18} />}
</ActionIcon>
);
};// file: /column-filter.tsx
import React, { useState } from "react";
import { Column } from "@tanstack/react-table";
import { TextInput, Menu, ActionIcon, Stack, Group } from "@mantine/core";
import { IconFilter, IconX, IconCheck } from "@tabler/icons-react";
interface ColumnButtonProps {
column: Column<any, any>; // eslint-disable-line
}
export const ColumnFilter: React.FC<ColumnButtonProps> = ({ column }) => {
// eslint-disable-next-line
const [state, setState] = useState(null as null | { value: any });
if (!column.getCanFilter()) {
return null;
}
const open = () =>
setState({
value: column.getFilterValue(),
});
const close = () => setState(null);
// eslint-disable-next-line
const change = (value: any) => setState({ value });
const clear = () => {
column.setFilterValue(undefined);
close();
};
const save = () => {
if (!state) return;
column.setFilterValue(state.value);
close();
};
const renderFilterElement = () => {
// eslint-disable-next-line
const FilterComponent = (column.columnDef?.meta as any)?.filterElement;
if (!FilterComponent && !!state) {
return (
<TextInput
autoComplete="off"
value={state.value}
onChange={(e) => change(e.target.value)}
/>
);
}
return <FilterComponent value={state?.value} onChange={change} />;
};
return (
<Menu
opened={!!state}
position="bottom"
withArrow
transition="scale-y"
shadow="xl"
onClose={close}
width="256px"
withinPortal
>
<Menu.Target>
<ActionIcon
size="xs"
onClick={open}
variant={column.getIsFiltered() ? "light" : "transparent"}
color={column.getIsFiltered() ? "primary" : "gray"}
>
<IconFilter size={18} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
{!!state && (
<Stack p="xs" spacing="xs">
{renderFilterElement()}
<Group position="right" spacing={6} noWrap>
<ActionIcon
size="md"
color="gray"
variant="outline"
onClick={clear}
>
<IconX size={18} />
</ActionIcon>
<ActionIcon
size="md"
onClick={save}
color="primary"
variant="outline"
>
<IconCheck size={18} />
</ActionIcon>
</Group>
</Stack>
)}
</Menu.Dropdown>
</Menu>
);
};Chakra UITanStack Table
Code Example
// file: /App.tsx
import React from "react";
import { Refine } from "@refinedev/core";
import dataProvider from "@refinedev/simple-rest";
import { ChakraProvider } from "@chakra-ui/react";
import { ProductTable } from "./product-table.tsx";
const API_URL = "https://api.fake-rest.refine.dev";
export default function App() {
return (
<ChakraProvider>
<Refine dataProvider={dataProvider(API_URL)}>
<ProductTable />
</Refine>
</ChakraProvider>
);
}// file: /product-table.tsx
import React from "react";
import { useTable } from "@refinedev/react-table";
import { ColumnDef, flexRender } from "@tanstack/react-table";
import {
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
HStack,
Text,
} from "@chakra-ui/react";
import { Pagination } from "./pagination";
import { ColumnSorter } from "./column-sorter";
import { ColumnFilter } from "./column-filter";
export const ProductTable: React.FC = () => {
const columns = React.useMemo<ColumnDef<IProduct>[]>(
() => [
{
id: "id",
header: "ID",
accessorKey: "id",
meta: {
filterOperator: "eq",
},
},
{
id: "name",
header: "Name",
accessorKey: "name",
meta: {
filterOperator: "contains",
},
},
{
id: "price",
header: "Price",
accessorKey: "price",
meta: {
filterOperator: "eq",
},
},
],
[],
);
const {
getHeaderGroups,
getRowModel,
refineCore: { setCurrentPage, pageCount, currentPage },
} = useTable({
refineCoreProps: {
resource: "products",
},
columns,
});
return (
<div style={{ padding:"8px" }}>
<Text fontSize='3xl'>Products</Text>
<TableContainer whiteSpace="pre-line">
<Table variant="simple">
<Thead>
{getHeaderGroups().map((headerGroup) => (
<Tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<Th key={header.id}>
{!header.isPlaceholder && (
<HStack spacing="2">
<Text>
{flexRender(
header.column.columnDef
.header,
header.getContext(),
)}
</Text>
<HStack spacing="2">
<ColumnSorter
column={header.column}
/>
<ColumnFilter
column={header.column}
/>
</HStack>
</HStack>
)}
</Th>
))}
</Tr>
))}
</Thead>
<Tbody>
{getRowModel().rows.map((row) => (
<Tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<Td key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</Td>
))}
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
<Pagination
currentPage={currentPage}
pageCount={pageCount}
setCurrentPage={setCurrentPage}
/>
</div>
);
};
interface IProduct {
id: number;
name: string;
price: string;
}// file: /pagination.tsx
import { FC } from "react";
import { HStack, Button, Box } from "@chakra-ui/react";
import { usePagination } from "@refinedev/chakra-ui";
export const Pagination: FC<PaginationProps> = ({
currentPage,
pageCount,
setCurrentPage,
}) => {
const pagination = usePagination({
currentPage,
pageCount,
});
return (
<Box display="flex" justifyContent="flex-end">
<HStack my="3" spacing="1">
{pagination?.prev && (
<Button
aria-label="previous page"
onClick={() => setCurrentPage(currentPage - 1)}
disabled={!pagination?.prev}
variant="outline"
>
Prev
</Button>
)}
{pagination?.items.map((page) => {
if (typeof page === "string")
return <span key={page}>...</span>;
return (
<Button
key={page}
onClick={() => setCurrentPage(page)}
variant={page === currentPage ? "solid" : "outline"}
>
{page}
</Button>
);
})}
{pagination?.next && (
<Button
aria-label="next page"
onClick={() => setCurrentPage(currentPage + 1)}
variant="outline"
>
Next
</Button>
)}
</HStack>
</Box>
);
};
type PaginationProps = {
currentPage: number;
pageCount: number;
setCurrentPage: (page: number) => void;
};// file: /column-sorter.tsx
import React, { useState } from "react";
import { IconButton } from "@chakra-ui/react";
import { IconChevronDown, IconChevronUp, IconSelector } from "@tabler/icons-react";
import type { SortDirection } from "@tanstack/react-table";
export interface ColumnButtonProps {
column: Column<any, any>; // eslint-disable-line
}
export const ColumnSorter: React.FC<ColumnButtonProps> = ({ column }) => {
if (!column.getCanSort()) {
return null;
}
const sorted = column.getIsSorted();
return (
<IconButton
aria-label="Sort"
size="xs"
onClick={column.getToggleSortingHandler()}
icon={<ColumnSorterIcon sorted={sorted} />}
variant={sorted ? "light" : "transparent"}
color={sorted ? "primary" : "gray"}
/>
);
};
const ColumnSorterIcon = ({ sorted }: { sorted: false | SortDirection }) => {
if (sorted === "asc") return <IconChevronDown size={18} />;
if (sorted === "desc") return <IconChevronUp size={18} />;
return <IconSelector size={18} />;
};// file: /column-filter.tsx
import React, { useState } from "react";
import {
Input,
Menu,
IconButton,
MenuButton,
MenuList,
VStack,
HStack,
} from "@chakra-ui/react";
import { IconFilter, IconX, IconCheck } from "@tabler/icons-react";
interface ColumnButtonProps {
column: Column<any, any>; // eslint-disable-line
}
export const ColumnFilter: React.FC<ColumnButtonProps> = ({ column }) => {
// eslint-disable-next-line
const [state, setState] = useState(null as null | { value: any });
if (!column.getCanFilter()) {
return null;
}
const open = () =>
setState({
value: column.getFilterValue(),
});
const close = () => setState(null);
// eslint-disable-next-line
const change = (value: any) => setState({ value });
const clear = () => {
column.setFilterValue(undefined);
close();
};
const save = () => {
if (!state) return;
column.setFilterValue(state.value);
close();
};
const renderFilterElement = () => {
// eslint-disable-next-line
const FilterComponent = (column.columnDef?.meta as any)?.filterElement;
if (!FilterComponent && !!state) {
return (
<Input
borderRadius="md"
size="sm"
autoComplete="off"
value={state.value}
onChange={(e) => change(e.target.value)}
/>
);
}
return (
<FilterComponent
value={state?.value}
onChange={(e: any) => change(e.target.value)}
/>
);
};
return (
<Menu isOpen={!!state} onClose={close}>
<MenuButton
onClick={open}
as={IconButton}
aria-label="Options"
icon={<IconFilter size="16" />}
variant="ghost"
size="xs"
/>
<MenuList p="2">
{!!state && (
<VStack align="flex-start">
{renderFilterElement()}
<HStack spacing="1">
<IconButton
aria-label="Clear"
size="sm"
colorScheme="red"
onClick={clear}
>
<IconX size={18} />
</IconButton>
<IconButton
aria-label="Save"
size="sm"
onClick={save}
colorScheme="green"
>
<IconCheck size={18} />
</IconButton>
</HStack>
</VStack>
)}
</MenuList>
</Menu>
);
};