Tables
Tables are essential in data-intensive applications, serving as the primary way for organizing and displaying data in a readable format using rows and columns. Their integration, however, is complex due to functionalities like sorting, filtering, and pagination. Refine's tables integration aims to make this process as simple as possible while providing as many real world features as possible out of the box. This guide will cover the basics of tables in Refine and how to use them.
Handling Data
useTable allows us to fetch data according to the sorter, filter, and pagination states. Under the hood, it uses useList for the fetch. Its designed to be headless, but Refine offers seamless integration with several popular UI libraries, simplifying the use of their table components.
- TanStack Table (for Headless, Chakra UI, Mantine) - Documentation) - Example
- Ant Design Table - Documentation - Example
- Material UI DataGrid - Documentation - Example
Basic Usage
The usage of the useTable hooks may slightly differ between UI libraries, however, the core functionality of useTable hook in @refinedev/core stays consistent in all implementations. The useTable hook in Refine's core is the foundation of all the other useTable implementations.
- Refine's Core
- TanStack Table
- Ant Design
- Material UI
- MantineTanStack Table
- Chakra UITanStack Table
import React from "react";import { useTable, pageCount, pageSize, current, setCurrent } from "@refinedev/core";export const ProductTable: React.FC = () => {const { tableQuery, pageCount, pageSize, current, setCurrent } = useTable<IProduct>({resource: "products",pagination: {current: 1,pageSize: 10,},});const posts = tableQuery?.data?.data ?? [];if (tableQuery?.isLoading) {return <div>Loading...</div>;}return (<div style={{ padding:"8px" }}><h1>Products</h1><table><thead><tr><th>ID</th><th>Name</th><th>Price</th></tr></thead><tbody>{posts.map((post) => (<tr key={post.id}><td>{post.id}</td><td>{post.name}</td><td>{post.price}</td></tr>))}</tbody></table><hr /><p>Current Page: {current}</p><p>Page Size: {pageSize}</p><buttononClick={() => {setCurrent(current - 1);}}disabled={current < 2}>Previous Page</button><buttononClick={() => {setCurrent(current + 1);}}disabled={current === pageCount}>Next Page</button></div>);};interface IProduct {id: number;name: string;price: string;}
Check out Refine's useTable reference page to learn more about the usage and see it in action.
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 : (<><divonClick={header.column.getToggleSortingHandler()}>{flexRender(header.column.columnDef.header,header.getContext(),)}{{asc: " 🔼",desc: " 🔽",}[header.column.getIsSorted() as string] ?? " ↕️"}</div></>)}{header.column.getCanFilter() ? (<div><inputvalue={(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><buttononClick={() => setPageIndex(0)}disabled={!getCanPreviousPage()}>{"<<"}</button><buttononClick={() => previousPage()}disabled={!getCanPreviousPage()}>{"<"}</button><button onClick={() => nextPage()} disabled={!getCanNextPage()}>{">"}</button><buttononClick={() => setPageIndex(getPageCount() - 1)}disabled={!getCanNextPage()}>{">>"}</button><span>Page<strong>{getState().pagination.pageIndex + 1} of{" "}{getPageCount()}</strong></span><span>| Go to page:<inputtype="number"defaultValue={getState().pagination.pageIndex + 1}onChange={(e) => {const page = e.target.value? Number(e.target.value) - 1: 0;setPageIndex(page);}}/></span>{" "}<selectvalue={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;}
import React from "react";import { useTable, FilterDropdown } from "@refinedev/antd";import { Table, Input } from "antd";export const ProductTable: React.FC = () => {const { tableProps } = useTable<IProduct>({resource: "products",filters: {initial: [{field: "name",operator: "contains",value: "",},],},});return (<div style={{ padding: "4px" }}><h2>Products</h2><Table {...tableProps} rowKey="id"><Table.ColumndataIndex="id"title="ID"sorter={{ multiple: 2 }}/><Table.ColumndataIndex="name"title="Name"filterDropdown={(props) => (<FilterDropdown {...props}><Input placeholder="Search by name" /></FilterDropdown>)}/><Table.ColumndataIndex="price"title="Price"sorter={{ multiple: 1 }}/></Table></div>);};interface IProduct {id: number;name: string;price: string;material: string;}
Check out Ant Design's useTable reference page to learn more about the usage and see it in action.
import React from "react";import { useDataGrid } from "@refinedev/mui";import { DataGrid, GridColDef } from "@mui/x-data-grid";export const ProductTable: React.FC = () => {const { dataGridProps } = useDataGrid<IProduct>({resource: "products",});const columns = React.useMemo<GridColDef<IProduct>[]>(() => [{field: "id",headerName: "ID",type: "number",width: 50,},{ field: "name", headerName: "Name", minWidth: 400, flex: 1 },{ field: "price", headerName: "Price", minWidth: 120, flex: 0.3 },],[],);return (<div style={{ padding:"4px" }}><h2>Products</h2><DataGrid {...dataGridProps} columns={columns} /></div>);};interface IProduct {id: number;name: string;price: string;}
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: { setCurrent, pageCount, current },} = 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><ColumnSortercolumn={header.column}/><ColumnFiltercolumn={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 /><Paginationposition="right"total={pageCount}page={current}onChange={setCurrent}/></div>);};interface IProduct {id: number;name: string;price: string;}
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: { setCurrent, pageCount, current },} = 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"><ColumnSortercolumn={header.column}/><ColumnFiltercolumn={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><Paginationcurrent={current}pageCount={pageCount}setCurrent={setCurrent}/></div>);};interface IProduct {id: number;name: string;price: string;}
Pagination
useTable has a pagination feature. The pagination is done by passing the current, pageSize and, mode keys to pagination object.
- current: The page index.
- pageSize: The number of items per page.
- mode: Whether to use server side pagination or not.
- When
serveris selected, the pagination will be handled on the server side. - When
clientis selected, the pagination will be handled on the client side. No request will be sent to the server. - When
offis selected, the pagination will be disabled. All data will be fetched from the server.
- When
You can also change the current and pageSize values by using the setCurrent and setPageSize functions that are returned by the useTable hook. Every change will trigger a new fetch.
import React from "react";import { useTable } from "@refinedev/core";export const ProductTable: React.FC = () => {const { tableQuery, pageCount, pageSize, current, setCurrent } = useTable<IProduct>({resource: "products",pagination: {current: 1,pageSize: 10,mode: "server", // "client" or "server"},});const posts = tableQuery?.data?.data ?? [];if (tableQuery?.isLoading) {return <div>Loading...</div>;}return (<div><h1>Products</h1><table><thead><tr><th>ID</th><th>Name</th><th>Price</th></tr></thead><tbody>{posts.map((post) => (<tr key={post.id}><td>{post.id}</td><td>{post.name}</td><td>{post.price}</td></tr>))}</tbody></table><hr /><p>Current Page: {current}</p><p>Page Size: {pageSize}</p><buttononClick={() => {setCurrent(current - 1);}}disabled={current < 2}>Previous Page</button><buttononClick={() => {setCurrent(current + 1);}}disabled={current === pageCount}>Next Page</button></div>);};interface IProduct {id: number;name: string;price: string;}
Filtering
useTable has a filter feature. The filter is done by using the initial, permanent, defaultBehavior and mode keys to filters object.
These states are a CrudFilters type for creating complex single or multiple queries.
- initial: The initial filter state. It can be changed by the
setFiltersfunction. - permanent: The default and unchangeable filter state. It can't be changed by the
setFiltersfunction. - defaultBehavior: The default behavior of the
setFiltersfunction.- When
mergeis selected, the new filters will be merged with the old ones. - When
replaceis selected, the new filters will replace the old ones. It means that the old filters will be deleted.
- When
- mode: Whether to use server side filter or not.
- When
serveris selected, the filters will be sent to the server. - When
offis selected, the filters will be applied on the client side.
- When
useTable will pass these states to dataProvider for making it possible to fetch the data you need. Handling and adapting these states for API requests is the responsibility of the dataProvider
import React from "react";import { useTable } from "@refinedev/core";export const ProductTable: React.FC = () => {const { tableQuery, filters, setFilters } = useTable<IProduct>({resource: "products",filters: {permanent: [{field: "price",value: "200",operator: "lte",},],initial: [{ field: "category.id", operator: "eq", value: "1" }],},});const products = tableQuery?.data?.data ?? [];const getFilterByField = (field: string) => {return filters.find((filter) => {if ("field" in filter && filter.field === field) {return filter;}}) as LogicalFilter | undefined;};const resetFilters = () => {setFilters([], "replace");};if (tableQuery.isLoading) {return <div>Loading...</div>;}return (<div><h1>Products with price less than 200</h1><table><thead><tr><th>ID</th><th>Name</th><th>Price</th><th>categoryId</th></tr></thead><tbody>{products.map((product) => (<tr key={product.id}><td>{product.id}</td><td>{product.name}</td><td>{product.price}</td><td>{product.category.id}</td></tr>))}</tbody></table><hr />Filtering by field:<b>{getFilterByField("category.id")?.field}, operator{" "}{getFilterByField("category.id")?.operator}, value{getFilterByField("category.id")?.value}</b><br /><buttononClick={() => {setFilters([{field: "category.id",operator: "eq",value:getFilterByField("category.id")?.value === "1"? "2": "1",},]);}}>Toggle Filter</button><button onClick={resetFilters}>Reset filter</button></div>);};interface IProduct {id: number;name: string;price: string;category: {id: number;};}
Sorting
useTable has a sorter feature. The sorter is done by passing the initial and permanent keys to sorters object. These states are a CrudSorter type for creating single or multiple queries.
- initial: The initial sorter state. It can be changed by the
setSortersfunction. - permanent: The default and unchangeable sorter state. It can't be changed by the
setSortersfunction.
useTable will pass these states to dataProvider for making it possible to fetch the data you need. Handling and adapting these states for API requests is the responsibility of the dataProvider
You can change the sorters state by using the setSorters function. Every change will trigger a new fetch.
import React from "react";import { useTable } from "@refinedev/core";export const ProductTable: React.FC = () => {const { tableQuery, sorters, setSorters } = useTable<IProduct>({resource: "products",sorters: {initial: [{ field: "price", order: "asc" }],},});const products = tableQuery?.data?.data ?? [];const findSorterByFieldName = (fieldName: string) => {return sorters.find((sorter) => sorter.field === fieldName);};if (tableQuery.isLoading) {return <div>Loading...</div>;}return (<div><h1>Products</h1><table><thead><tr><th>ID</th><th>Name</th><th>Price</th></tr></thead><tbody>{products.map((product) => (<tr key={product.id}><td>{product.id}</td><td>{product.name}</td><td>{product.price}</td></tr>))}</tbody></table><hr /><hr />Sorting by field:<b>{findSorterByFieldName("price")?.field}, order{" "}{findSorterByFieldName("price")?.order}</b><br /><buttononClick={() => {setSorters([{field: "price",order:findSorterByFieldName("price")?.order === "asc"? "desc": "asc",},]);}}>Toggle Sort</button></div>);};interface IProduct {id: number;name: string;price: string;}
Search
useTable has a search feature with onSearch. The search is done by using the onSearch function with searchFormProps. These feature enables you to easily connect form state to the table filters.
- onSearch: function is triggered when the
searchFormProps.onFinishis called. It receives the form values as the first argument and expects a promise that returns aCrudFilterstype. - searchFormProps: Has necessary props for the
<form>.
For example we can fetch product with the name that contains the search value.
- Ant Design
- Material UI
import React from "react";import { HttpError } from "@refinedev/core";import { useTable } from "@refinedev/antd";import { Button, Form, Input, Space, Table } from "antd";export const ProductTable: React.FC = () => {const { tableProps, searchFormProps } = useTable<IProduct,HttpError,IProduct>({resource: "products",onSearch: (values) => {return [{field: "name",operator: "contains",value: values.name,},];},});return (<div style={{ padding: "4px" }}><h2>Products</h2><Form {...searchFormProps}><Space><Form.Item name="name"><Input placeholder="Search by name" /></Form.Item><Form.Item><Button htmlType="submit">Search</Button></Form.Item></Space></Form><Table {...tableProps} rowKey="id"><Table.Column dataIndex="id" title="ID" /><Table.Column dataIndex="name" title="Name" /><Table.Column dataIndex="price" title="Price" /></Table></div>);};interface IProduct {id: number;name: string;price: string;}
Check out Ant Design's useTable reference page to learn more about the usage and see it in action.
import React from "react";import { useDataGrid } from "@refinedev/mui";import { DataGrid, GridColDef } from "@mui/x-data-grid";import { HttpError } from "@refinedev/core";import Typography from "@mui/material/Typography";import Box from "@mui/material/Box";import Button from "@mui/material/Button";import Input from "@mui/material/Input";export const ProductTable: React.FC = () => {const { dataGridProps, search } = useDataGrid<IProduct,HttpError,Partial<IProduct>>({onSearch: (values) => {return [{field: "name",operator: "contains",value: values.name,},];},resource: "products",});const columns = React.useMemo<GridColDef<IProduct>[]>(() => [{field: "id",headerName: "ID",type: "number",width: 50,},{ field: "name", headerName: "Name", minWidth: 400, flex: 1 },{ field: "price", headerName: "Price", minWidth: 120, flex: 0.3 },],[],);return (<div style={{ padding: "4px" }}><Typography variant="h4" component="h2">Products</Typography><Box sx={{ mt: 2 }}><formonSubmit={(e) => {e.preventDefault();const target = e.target as typeof e.target & {name: { value: string };};search({ name: target.name.value });}}><Input placeholder="Search by name" name="name" /><Button type="submit">Search</Button></form></Box><DataGrid {...dataGridProps} columns={columns} sx={{ mt: 2 }} /></div>);};interface IProduct {id: number;name: string;price: string;}
Integrating with Routers
Resource Router IntegratedThis value can be inferred from the route. Click to see the guide for more information.
useTable can infer current resource from the current route based on your resource definitions. This eliminates the need of passing these parameters to the hooks manually.
useTable({
// When the current route is `/products`, the resource prop can be omitted.
resource: "products",
});
Sync with Location Router IntegratedThis value can be inferred from the route. Click to see the guide for more information. Globally ConfigurableThis value can be configured globally. Click to see the guide for more information.
When you use the syncWithLocation feature, the useTable's state (e.g., sort order, filters, pagination) is automatically encoded in the query parameters of the URL, and when the URL changes, the useTable state is automatically updated to match. This makes it easy to share table state across different routes or pages, and to allow users to bookmark or share links to specific table views.
Relationships
Refine handles data relations with data hooks(eg: useOne, useMany, etc.). This compositional design allows you to flexibly and efficiently manage data relationships to suit your specific requirements.
For example imagine each post has a many category. We can fetch the categories of the post by using the useMany hook.
import React from "react";import { useTable, HttpError, useMany } from "@refinedev/core";export const HomePage: React.FC = () => {const { tableQuery } = useTable<IPost, HttpError>({resource: "posts",});const posts = tableQuery?.data?.data ?? [];const categoryIds = posts.map((item) => item.category.id);const { data: categoriesData, isLoading } = useMany<ICategory>({resource: "categories",ids: categoryIds,queryOptions: {enabled: categoryIds.length > 0,},});if (tableQuery?.isLoading) {return <div>Loading...</div>;}return (<div><h1>Posts</h1><table><thead><tr><th>ID</th><th>Title</th><th>Category</th></tr></thead><tbody>{posts.map((post) => (<tr key={post.id}><td>{post.id}</td><td>{post.title}</td><td>{isLoading ? (<div>Loading...</div>) : (categoriesData?.data.find((item) => item.id === post.category.id,)?.title)}</td></tr>))}</tbody></table></div>);};interface IPost {id: number;title: string;category: {id: number;};}interface ICategory {id: number;title: string;}