DataGrid
Fully-featured data table component. Built on top of Tanstack's React Table.
| Name | Price | Category | Availability | Date | Visible | 
|---|
Installation
npx @rahimstack@latest add datagridUsage
Define the type
First, you need to define the type of the data that will be displayed.
Example
type Product = {
    id: string
    name: string
    image: string
    visible: boolean
    availability: "in_stock" | "out_of_stock"
    price: number
    category: string | null
}Column definition
- You can define your columns using the defineDataGridColumnshelper method. This method takes a callback function that returns an array of typeColumnDef.
- Learn more about TanStack Table column definition.
Example
import { defineDataGridColumns } from "@/components/ui/datagrid"
 
type Product = {
    // ...
}
 
function Demo() {
    const columns = React.useMemo(() => defineDataGridColumns<Product>(() => [
        {
            accessorKey: "name",
            header: "Name",
        },
        {
            accessorKey: "price",
            header: "Price",
            cell: info => "$" + Intl.NumberFormat("en-US").format(info.getValue<number>()),
        },
        // ...
    ]), [])
 
    return <></>
}Render the table
import { defineDataGridColumns, DataGrid } from "@/components/ui/datagrid"
import { fetchFakeData } from "./datagrid-fake-api"
import * as React from "react"
 
export type Product = {
    id: string
    name: string
    image: string
    visible: boolean
    availability: "in_stock" | "out_of_stock"
    price: number
    category: string | null
}
 
export function DataGridMinimalExample() {
 
    const [clientData, setClientData] = React.useState<Product[] | undefined>(undefined)
 
    React.useEffect(() => {
        (async function() {
            const res = await fetchFakeData()
            setClientData(res.rows)
        })()
    }, [])
 
    const columns = React.useMemo(() => defineDataGridColumns<Product>(() => [
        {
            accessorKey: "name",
            header: "Name",
            size: 60,
        },
        {
            accessorKey: "price",
            header: "Price",
            cell: info => "$" + Intl.NumberFormat("en-US").format(info.getValue<number>()),
            size: 40,
        },
        // ...
    ]), [])
 
    return (
        <DataGrid<Product>
            columns={columns}
            data={clientData}
            rowCount={clientData?.length ?? 0}
            isLoading={!clientData}
        />
    )
 
}Loading state
- isLoading: When data is coming in for the first time
- isDataMutating: When data is already present
Initial state
You can pass an initial state to the DataGrid component. This is useful when you want to control the state of the DataGrid from outside.
<DataGrid<T>
    {...props}
    initialState={{
        globalFilter: "",
        sorting: [],
        pagination: { pageIndex: 0, pageSize: 5 },
        rowSelection: {},
        columnFilters: [],
        columnVisibility: {},
    }}
/>Pagination
Pagination is handled on the client by default.
- rowCountis used to calculate the page count based on the- pageSize. This value should be the number of rows fetched.- If you are using server-side pagination, the value should reflect the total number of rows in the database based on the filters.
 
Server-side/Manual
- You should update rowCountprogrammatically based on the data fetched.
- Set enableManualPaginationtotrueand track the current state manually.
import { PaginationState } from "@tanstack/react-table"
 
// ...
const [pagination, setPagination] = React.useState<PaginationState>({ pageIndex: 0, pageSize: 5 })
 
// ...
 
const { data, isLoading } = useFakeQuery({
    queryKey: ["products", pagination],
    queryFn: async () => {
        const fetchURL = new URL(/*...*/)
        fetchURL.searchParams.set("pageIndex", String(pagination.pageIndex))
        fetchURL.searchParams.set("limit", String(pagination.pageSize))
        const res = await fetch(fetchURL.href)
        return (await res.json())
    },
})
 
return (
    <DataGrid<T>
        columns={columns}
        data={data}
        rowCount={data?.length ?? 0}
        isLoading={isLoading}
        enableManualPagination={true}
        initialState={{ pagination }}
        onPaginationChange={setPagination}
    />
)Sorting
Sorting is automatically handled on a column-to-column basis.
Disable sorting
Column-level
const columns = useMemo(() => defineDataGridColumns<Product>(() => [
    //...
    {
        id: "_actions",
        enableSorting: false,
        enableGlobalFilter: false,
        cell: ({ row }) => <div className={"flex justify-end w-full"}>{/*...*/}</div>
    },
    //...
]), [])Globally
<DataGrid<Product>
    {...props}
    enableSorting={false}
/>Server-side/Manual
import { SortingState } from "@tanstack/react-table"
 
// ...
const [sorting, setSorting] = useState<SortingState>([])
 
// ...
 
const { data, isLoading } = useFakeQuery({
    queryKey: ["products", sorting],
    queryFn: async () => {
        const fetchURL = new URL(/*...*/)
        // Handle sending the sorting state to the server
        const res = await fetch(fetchURL.href)
        return (await res.json())
    },
})
 
return (
    <DataGrid<T>
        {...props}
        initialState={{ sorting }}
        onSortingChange={setSorting}
    />
)Row selection
<DataGrid<T>
    {...props}
    enableRowSelection={true}
    onRowSelect={data => {
        console.log(data)
    }}
/>Server-side/Manual
By default, row selection only works on rows that are fetched.
- Set enablePersistentRowSelectiontotrueto enable row selection on all rows.
- You will have to provide a rowSelectionPrimaryKeyin order to uniquely identify the rows.
<DataGrid<T>
    {...props}
    enableManualPagination={true}
    enableRowSelection={true}
    enablePersistentRowSelection={true}
    rowSelectionPrimaryKey="id" // or any other unique key
    onRowSelect={data => {
        console.log(data)
    }}
/>Filtering
By default, filtering is handled on the client, so only the rows that are fetched will be filtered.
Enable filtering on specific columns using the withFiltering and getFilterFn helpers.
Filter types
These are the pre-built filters
- select
- radio
- checkbox
- boolean
- date-range
- number
You will have to provide both a filterFn prop for each column you want to enable filtering on by using the getFilterFn helper,
and a meta prop with the filter configuration using the withFiltering helper.
const columns = useMemo(() => defineDataGridColumns<Product>(({ withValueFormatter, withFiltering, getFilterFn }) => [
    //...
    {
        accessorKey: "category",
        header: "Category",
        cell: info => info.getValue(),
        size: 20,
        filterFn: getFilterFn("radio"),
        meta: {
            ...withFiltering({
                name: "Category",
                type: "radio",
                options: [{ value: "Electronics" }, { value: "Food" }],
                icon: <BiFolder/>,
            }),
        },
    },
    {
        accessorKey: "availability",
        header: "Availability",
        cell: info => info.getValue(),
        size: 20,
        filterFn: getFilterFn("checkbox"),
        meta: {
            ...withValueFormatter<string>(value => { // Format the value for the filter buttons
                if (value === "out_of_stock") return "Out of stock"
                else if (value === "in_stock") return "In stock"
                return value
            }),
            ...withFiltering({
                name: "Availability",
                type: "checkbox",
                icon: <BiCheck/>,
                options: [
                    { value: "out_of_stock", label: "Out of stock" },
                    { value: "in_stock", label: "In stock" },
                ],
            }),
        },
    },
    {
        accessorKey: "visible",
        header: "Visible",
        cell: info => <Badge intent={info.getValue() === "Visible" ? "success" : "gray"}>{info.getValue<string>()}</Badge>,
        size: 20,
        filterFn: getFilterFn("boolean"),
        meta: {
            ...withValueFormatter<boolean, string>(value => value ? "Visible" : "Hidden"),
            ...withFiltering({
                name: "Visible",
                type: "boolean",
                icon: <BiLowVision/>,
                valueFormatter: (value) => { // Overrides `withValueFormatter`
                    return value ? "Yes" : "No"
                },
            }),
        },
    },
    ...
]), [])Disable filtering
<DataGrid<Product>
    {...props}
    enableColumnFilters={false}
    enableGlobalFilter={false}
/>Server-side/Manual
Filtering can be done server-side by setting enableManualFiltering to true.
import { ColumnFiltersState } from "@tanstack/react-table"
 
// ...
const [globalFilter, setGlobalFilter] = React.useState<string>("")
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
 
// ...
 
const { data, totalCount, isLoading } = useFakeQuery({
    queryKey: ["products", globalFilter, columnFilters],
    queryFn: async () => {
        const fetchURL = new URL(/*...*/)
        fetchURL.searchParams.set("nameLike", globalFilter)
        fetchURL.searchParams.set("category", columnFilters.find(f => f.id === "category")?.value as string ?? "")
        fetchURL.searchParams.set("visibility", String(columnFilters.find(f => f.id === "visibility")?.value ?? ""))
        // ...
        const res = await fetch(fetchURL.href)
        return (await res.json())
    },
})
 
return (
    <DataGrid<T>
        columns={columns}
        data={data}
        rowCount={totalCount}
        isLoading={isLoading}
        enableManualPagination={true}
        initialState={{ globalFilter, columnFilters }}
        onGlobalFilterChange={setGlobalFilter}
        onColumnFiltersChange={setColumnFilters}
    />
)Column Visibility
You can hide certain columns below a specific pixel width. The width is based on the table element.
<DataGrid<Product>
    {...props}
    hideColumns={[
        { below: 850, hide: ["availability", "price"] },
        { below: 600, hide: ["_actions"] },
        { below: 515, hide: ["category"] },
        { below: 400, hide: ["visible"] },
    ]}
/>Editing
Making a column editable
- Only one row can be edited at a time. Users can edit multiple cells in a row simultaneously.
- Use the onRowEditcallback function prop to get access to the updated row data.
const columns = useMemo(() => defineDataGridColumns<Product>(({ withEditing }) => [
    {
        accessorKey: "name",
        header: "Name",
        cell: info => info.getValue(),
        meta: {
            ...withEditing<string>({
                field: ({ onChange, ...ctx }) => (
                    <TextInput {...ctx} onTextChange={onChange} intent="unstyled" />
                ),
            }),
        },
    },
    // ...
]), [])- withEditingaccepts a- fieldprop that is used to render the input field.
- fieldis a callback function that returns a React element. It has the following parameters:- context: An object containing a- ref,- value, and- onChangefunction to control the input field.
- options: An object containing the row errors- rowErrors: DataGridValidationRowErrors,- table,- row, and- cellinstances.
 
You can specify the type of the field using the zodType prop on withEditing or by using generics.
const schema = defineSchema(({z}) => z.object({
    name: z.string(),
    ...
}))
 
// Column def
 
meta: {
    ...withEditing({
        zodType: schema.shape.name, // Define type from zod
        field: (ctx) => {
            console.log(ctx.value) // string
        },
    }),
}
 
meta: {
    ...withEditing<string>({ // Or define it from generics
        field: (ctx) => {
            console.log(ctx.value) // string
        },
    }),
}Example
const columns = useMemo(() => defineDataGridColumns<Product>(({ withEditing }) => [
    {
        accessorKey: "name",
        header: "Name",
        cell: info => info.getValue(),
        meta: {
            ...withEditing<string>({
                field: ({ onChange, ...ctx }, { rowErrors, row }) => {
                    // Get field's error
                    const error = rowErrors.find(n => n.key === "name" && n.rowId === row.id)
                    
                    return <TextInput {...ctx} onTextChange={onChange} intent="unstyled" />
                },
            }),
        },
    },
    // ...
]), [])Validation
- You can pass a zod schema to the DataGrid component using the validationSchemaprop.
- By using the zod schema, you can validate the data before onRowEditis called on the client or on the server.
- You can also use the onRowValidationErrorcallback function prop to get access to the validation errors.
const schema = defineSchema(({ z }) => z.object({
    name: z.string().min(3), // The name cell value will be validated against this schema
}))
 
const columns = useMemo(() => defineDataGridColumns<Product>(({ withEditing }) => [
    {
        accessorKey: "name",
        header: "Name",
        meta: {
            ...withEditing<string>({
                field: (ctx) => {/*...*/},
            }),
        }
    },
    ...rest
]), [])
 
return (
    <DataGrid<T>
        {...props}
        validationSchema={schema}
        onRowEdit={({ data, originalData, row }) => {
            // ...
        }}
        onRowValidationError={({ errors }) => {
            errors.forEach(error => {
                toast.error(error.path.join("."), {
                    description: error.message,
                })
            })
        }}
    />
)Event
onRowEdit will be called when the user clicks the save button and the row data is valid.
- data: The row data containing the updated values
- originalData: The original row data
- row: Row API. See reference.
<DataGrid<T>
    {...props}
    onRowEdit={({ data, originalData, row }) => {
        // ...
    }}
/>Optimistic updates
- Set enableOptimisticUpdatestotrue
- Provide a optimisticUpdatePrimaryKeythat will be used by the DataGrid to locate the row and perform the optimistic updates
- Handle potential errors by refetching/refreshing the data in case of an internal server error
- When optimistic updates are enabled, DataGrid will not display a loading state when isDataMutatingis true
Note: Optimistic updates are paused when server-side validation is done using the zod schema.
Server-side validation
const { data, isLoading, refetch } = useFakeQuery({ ...queryOptions })
 
const { mutate, isMutating } = useFakeMutation({ ...mutationOptions })
 
const [isValidating, setIsValidating] = useState(false)
const serverSideNameValidation = React.useCallback(async (value: string) => {
    setIsValidating(true)
    const res = await verifyNameIsUnique(value) // Server-side validation
    setIsValidating(false)
    return res.isUnique
}, [])
 
const schema = defineSchema(({z}) => z.object({
    name: z.string().refine(value => serverSideNameValidation(value), { message: "Name already exists" }),
    // ...
}))
 
return (
    <DataGrid<Product>
        {...props}
        isLoading={isLoading}
        isDataMutating={isMutating || isValidating}
        validationSchema={schema}
        enableOptimisticUpdates={true}
        onRowEdit={({ data, originalData, row }) => {
            mutate(data, {
                onSuccess: () => refetch(),
                onError: () => refetch(),
            })
        }}
        onRowValidationError={({ errors }) => {
            // ...
        }}
    />
)Server-side example
| Name | Price | Category | Availability | Visible | Date | 
|---|
API Reference
const DataGridAnatomy = defineStyleAnatomy({
    root: cva([
        "UI-DataGrid__root",
    ]),
    header: cva([
        "UI-DataGrid__header",
        "block space-y-4 w-full mb-4",
    ]),
    toolbar: cva([
        "UI-DataGrid__toolbar",
        "flex w-full items-center gap-4 flex-wrap",
    ]),
    tableContainer: cva([
        "UI-DataGrid__tableContainer",
        "align-middle inline-block min-w-full max-w-full overflow-x-auto relative",
    ]),
    table: cva([
        "UI-DataGrid__table",
        "w-full relative table-fixed",
    ]),
    tableHead: cva([
        "UI-DataGrid__tableHead",
        "border-b",
    ]),
    th: cva([
        "UI-DataGrid__th group/th",
        "px-3 h-12 text-left text-sm font-bold",
        "data-[is-selection-col=true]:px-3 data-[is-selection-col=true]:sm:px-1 data-[is-selection-col=true]:text-center",
    ]),
    titleChevronContainer: cva([
        "UI-DataGrid__titleChevronContainer",
        "absolute flex items-center inset-y-0 top-1 -right-9 group",
    ]),
    titleChevron: cva([
        "UI-DataGrid__titleChevron",
        "mr-3 h-4 w-4 text-gray-400 group-hover:text-gray-500 relative bottom-0.5",
    ]),
    tableBody: cva([
        "UI-DataGrid__tableBody",
        "divide-y divide-[--border] w-full relative",
    ]),
    td: cva([
        "UI-DataGrid__td",
        "px-2 py-2 w-full whitespace-nowrap text-base font-normal text-[--foreground]",
        "data-[is-selection-col=true]:px-2 data-[is-selection-col=true]:sm:px-0 data-[is-selection-col=true]:text-center",
        "data-[action-col=false]:truncate data-[action-col=false]:overflow-ellipsis",
        "data-[row-selected=true]:bg-brand-50 dark:data-[row-selected=true]:bg-gray-800",
        "data-[editing=true]:ring-1 data-[editing=true]:ring-[--ring] ring-inset",
        "data-[editable=true]:hover:bg-[--subtle] md:data-[editable=true]:focus:ring-2 md:data-[editable=true]:focus:ring-[--slate]",
        "focus:outline-none",
    ]),
    tr: cva([
        "UI-DataGrid__tr",
        "hover:bg-[--subtle] truncate",
    ]),
    footer: cva([
        "UI-DataGrid__footer",
        "flex flex-col sm:flex-row w-full items-center gap-2 justify-between p-2 mt-2 overflow-x-auto max-w-full",
    ]),
    footerPageDisplayContainer: cva([
        "UI-DataGrid__footerPageDisplayContainer",
        "flex flex-none items-center gap-1 ml-2 text-sm",
    ]),
    footerPaginationInputContainer: cva([
        "UI-DataGrid__footerPaginationInputContainer",
        "flex flex-none items-center gap-2",
    ]),
    filterDropdownButton: cva([
        "UI-DataGrid__filterDropdownButton",
        "flex gap-2 items-center bg-[--paper] border rounded-[--radius] h-10 py-1 px-3 cursor-pointer hover:bg-[--subtle]",
        "select-none focus-visible:ring-2 outline-none ring-[--ring]",
    ]),
    editingCard: cva([
        "UI-DataGrid__editingCard",
        "flex items-center gap-2 rounded-md px-3 py-2",
    ]),
})