Navigation
Inputs & Forms
Selection
Containers & Overlays
Overview
DataTable shows records in rows and columns: searchable, sortable, filterable and selectable. Rows are virtualized, so large data sets stay fast.
Name
Gender
Age
City
"use client";import {Button,ButtonGroup,ColumnConfig,DataTable,DataTableChip,IconAdd,IconArrowDown,IconArrowUp,IconFileCopy,IconEdit,IconMore2Alt,IconDeleteBin5,Link,LinkProps,Menu,RowConfig,SimpleTableOptions,useSimpleTable,useToolbar,} from "@brynlabs/fusion-ui";import React, {ReactNode, useCallback, useMemo, useState} from "react";export const extractTextFromElement = (obj: ReactNode): string => {if (typeof obj === "string") return obj;if (React.isValidElement(obj)) {// @ts-expect-error children are allowed to be undefinedreturn extractTextFromElement(obj.props.children);}if (Array.isArray(obj)) {return obj.map(e => extractTextFromElement(e)).join(" ");}return "";};export type User = {name: string | React.ReactElement<LinkProps>;gender: string;age: string | React.ReactElement<LinkProps>;birthday: string;height: string;weight: string;eyeColor: string;city: string | React.ReactElement<LinkProps>;origin: string;marriageStatus: string;alive: "Alive" | "Deceased";};type DataTableTestWrapperProps = {withMaleFilterButton?: boolean;};const data: User[] = [{name: "Max Müller",gender: "Male",age: "29",birthday: "1996-03-14",height: "182",weight: "78",eyeColor: "Blue",city: "Berlin",origin: "German",marriageStatus: "Single",alive: "Alive",},{name: "Sophia Schmidt",gender: "Female",age: "24",birthday: "2001-07-22",height: "168",weight: "60",eyeColor: "Green",city: "Berlin",origin: "German",marriageStatus: "Single",alive: "Alive",},{name: "Emma Johnson",gender: "Female",age: "31",birthday: "1994-11-30",height: "170",weight: "65",eyeColor: "Brown",city: (<Linkkey="newyork"link={{title: "New York City", href: "https://en.wikipedia.org/wiki/New_York_City"}}linkStyle="secondary">New York</Link>),origin: "American",marriageStatus: "Married",alive: "Alive",},{name: "Luca Rossi",gender: "Male",age: "27",birthday: "1998-05-18",height: "176",weight: "72",eyeColor: "Brown",city: "Rome",origin: "Italian",marriageStatus: "Widowed",alive: "Alive",},{name: "Marie Dubois",gender: "Female",age: "63",birthday: "1942-09-12",height: "165",weight: "58",eyeColor: "Blue",city: (<Linkkey="france"link={{title: "France", href: "https://en.wikipedia.org/wiki/France"}}linkStyle="secondary">French</Link>),origin: "French",marriageStatus: "Married",alive: "Deceased",},{name: (<Linkkey="jamesanderson"link={{title: "James Anderson",href: "https://en.wikipedia.org/wiki/James_Anderson_(cricketer)",}}linkStyle="secondary">James Anderson</Link>),gender: "Male",age: "35",birthday: "1990-01-09",height: "185",weight: "85",eyeColor: "Green",city: "New York",origin: "American",marriageStatus: "Divorced",alive: "Alive",},{name: "Anna Kowalski",gender: "Female",age: "29",birthday: "1996-06-25",height: "172",weight: "63",eyeColor: "Hazel",city: "Berlin",origin: "Polish",marriageStatus: "Single",alive: "Alive",},{name: "Carlos García",gender: "Male",age: "40",birthday: "1985-02-03",height: "180",weight: "82",eyeColor: "Brown",city: "Madrid",origin: "Spanish",marriageStatus: "Married",alive: "Alive",},{name: "Emily Clark",gender: "Female",age: "26",birthday: "1999-04-11",height: "169",weight: "59",eyeColor: "Blue",city: "Toronto",origin: "Canadian",marriageStatus: "Single",alive: "Alive",},{name: "Kenji Tanaka",gender: "Male",age: "32",birthday: "1993-08-19",height: "174",weight: "68",eyeColor: "Brown",city: "Tokyo",origin: "Japanese",marriageStatus: "Married",alive: "Alive",},{name: "Max Müller",gender: "Male",age: (<Linkkey="24"link={{title: "24", href: "https://en.wikipedia.org/wiki/24_(number)"}}linkStyle="secondary">24</Link>),birthday: "2001-01-10",height: "185",weight: "85",eyeColor: "Blue",city: "Hamburg",origin: "German",marriageStatus: "Single",alive: "Alive",},];const usersToAdd: User[] = [{name: "Olivia Brown",gender: "Female",age: "28",birthday: "1997-02-18",height: "171",weight: "62",eyeColor: "Brown",city: "London",origin: "British",marriageStatus: "Single",alive: "Alive",},{name: "Noah Wilson",gender: "Male",age: "34",birthday: "1991-10-05",height: "183",weight: "81",eyeColor: "Blue",city: "Dublin",origin: "Irish",marriageStatus: "Married",alive: "Alive",},{name: "Mia Svensson",gender: "Female",age: "30",birthday: "1995-06-12",height: "167",weight: "57",eyeColor: "Green",city: "Stockholm",origin: "Swedish",marriageStatus: "Single",alive: "Alive",},{name: "Ethan Novak",gender: "Male",age: "41",birthday: "1984-09-27",height: "179",weight: "76",eyeColor: "Hazel",city: "Prague",origin: "Czech",marriageStatus: "Divorced",alive: "Alive",},];const modalText = {errorMessages: {noDataFound: "No data was found",noDataFoundInSearch: "No searched results",noDataFoundInFilter: "No filtered results",noVisibleColumns: "No visible columns",},editView: {title: "Adjust columns",activeContainerTitle: "Visible Columns",inactiveContainerTitle: "Hidden Columns",activeContainerEmptyMessage: "No columns are visible",inactiveContainerEmptyMessage: "No columns are hidden",showAllButton: "Show all columns",applyButton: "Apply",abortViewText: {title: "Discard changes?",description:"You have unsaved changes. If you continue without saving, these changes will be lost.",confirmButton: "Discard",cancelButton: "Cancel",},},filterView: {title: "Adjust Filters",searchPlaceholder: "Type to search ...",min: "Minimum",max: "Maximum",applyButton: "Apply",resetButton: "Reset Filters",abortViewText: {title: "Discard changes?",description:"You have unsaved changes. If you continue without saving, these changes will be lost.",confirmButton: "Discard",cancelButton: "Cancel",},},};export const DataTableExample = ({withMaleFilterButton = false}: DataTableTestWrapperProps) => {const [users, setUsers] = useState<User[]>(data);const [nextMockUserIndex, setNextMockUserIndex] = useState(0);const addUser = useCallback(() => {const nextUser = usersToAdd[nextMockUserIndex % usersToAdd.length];if (!nextUser) return;setUsers(prev => [...prev, nextUser]);setNextMockUserIndex(prev => prev + 1);}, [nextMockUserIndex]);const tableOptions = useMemo<SimpleTableOptions<User>>(() => {const columns: ColumnConfig<User>[] = [{key: "name", label: "Name", render: row => row.name},{key: "gender",render: row => row.gender,label: "Gender",filter: {type: "multiSelect", getValue: row => row.gender},},{key: "age",render: row => row.age,label: "Age",sortFunction: (a, b) =>Number(extractTextFromElement(a.age)) - Number(extractTextFromElement(b.age)),filter: {type: "range",min: 0,max: 100,showTicks: true,histogram: Array.from({length: 10},(_, bucket) =>users.filter(user => {const age = Number(extractTextFromElement(user.age));return Math.min(Math.floor(age / 10), 9) === bucket;}).length,),getValue: row => Number(extractTextFromElement(row.age)),},},{key: "birthday",render: row => row.birthday,label: "Birthday",filter: {type: "custom",filterFunction: (row, value) => {if (!value || typeof value !== "object" || Array.isArray(value)) return true;const typedValue = value as {from?: string; to?: string};if (typedValue.from && row.birthday < typedValue.from) return false;return !(typedValue.to && row.birthday > typedValue.to);},},},{key: "height", render: row => row.height, label: "Height"},{key: "weight", render: row => row.weight, label: "Weight"},{key: "eyeColor",render: row => row.eyeColor,label: "Eye Color",filter: {type: "multiSelect", getValue: row => row.eyeColor},},{key: "city",render: row => row.city,label: "City",filter: {type: "multiSelect", getValue: row => extractTextFromElement(row.city)},},{key: "origin",render: row => row.origin,label: "Origin",filter: {type: "multiSelect", getValue: row => row.origin},},{key: "marriageStatus",render: row => row.marriageStatus,label: "Marriage Status",filter: {type: "multiSelect", getValue: row => row.marriageStatus},},{key: "alive",render: row => row.alive,label: "Alive",filter: {type: "boolean",trueLabel: "Alive",falseLabel: "Deceased",getValue: row => row.alive === "Alive",},},];return {columns,searchIndexRow: row => columns.map(column => extractTextFromElement(column.render(row))),initialColumns: ["name", "gender", "age", "city"],};}, [users]);const table = useSimpleTable<User>(users, tableOptions);const deleteRow = useCallback((rowIndex: number) => {setUsers(prev => prev.filter((_, index) => index !== rowIndex));}, []);const deleteSelectedRows = useCallback(() => {setUsers(prev => prev.filter((_, index) => !table.selectionBehavior.selection[index]));}, [table.selectionBehavior.selection]);const selectedCount = table.selectionBehavior.selectionSize;const getRowContextMenu = useCallback((rowIndex: number): Menu => [{items: [{title: "Duplicate",icon: <IconFileCopy />,callback: () => undefined,},{title: "Download file",icon: <IconArrowDown />,callback: () => undefined,},{title: "Delete",icon: <IconDeleteBin5 />,callback: () => deleteRow(rowIndex),},],},],[deleteRow],);const rows: RowConfig<User>[] = useMemo(() =>table.rows.map(item => ({...item,buttons: [{icon: <IconEdit />, action: () => undefined},{icon: <IconMore2Alt />, contextMenu: getRowContextMenu(item.index)},],})),[getRowContextMenu, table.rows],);const dataTableToolbarConfig = useMemo(() => ({segments: [{segment: [{section: `${selectedCount} selected`, hasDivider: true},{section: `Total: ${users.length}`},],},{segment: [{section: (<ButtonGroup><ButtonbuttonStyle="ghost"size="large"color="neutral"secondaryIcon={<IconArrowUp />}>Status</Button><ButtonbuttonStyle="ghost"size="large"color="neutral"secondaryIcon={<IconFileCopy />}>Duplicate</Button><ButtonbuttonStyle="ghost"size="large"color="neutral"secondaryIcon={<IconArrowDown />}>Export</Button></ButtonGroup>),hasDivider: true,hasSidePadding: false,},{section: (<ButtonbuttonStyle="flat"size="large"color="critical"primaryIcon={<IconDeleteBin5 />}onClick={deleteSelectedRows}>Delete</Button>),hasSidePadding: false,},],},],}),[deleteSelectedRows, selectedCount, users.length],);const dataTableToolbar = useToolbar(dataTableToolbarConfig);const columnsWithContextMenu: ColumnConfig<User>[] = useMemo(() => {const hiddenCategories = table.columns.filter(col => table.visibleColumns.findIndex(visible => col.key === visible) == -1,);return table.columns.map(column => ({...column,contextMenu: [{items: [{title: "Hide column",callback: () =>table.setVisibleColumns(prev => prev.filter(prevKey => prevKey !== column.key)),},{icon: <IconAdd />,title: "Show column to the right",children:hiddenCategories.length > 0? hiddenCategories.map(category => ({title: category.label,callback: () =>table.setVisibleColumns(prev => {const clickedIndex = prev.findIndex(category => category === column.key);if (clickedIndex === -1) return prev;return [...prev.slice(0, clickedIndex + 1),category.key,...prev.slice(clickedIndex + 1),];}),})): [{title: "No hidden columns",callback: () => undefined,},],},],},],}));}, [table]);const handleRemoveFilterChip = useCallback((chip: DataTableChip) => {const filterKey = chip.filterKey;const column = table.columns.find(column => String(column.key) === filterKey);if (!column?.filter) return;const currentValue = table.filters.values[filterKey];if (column.filter.type === "multiSelect") {const nextValues = (Array.isArray(currentValue) ? currentValue : []).filter(value => value !== chip.itemValue,);if (nextValues.length === 0) {table.filters.clearFilterValue(filterKey);} else {table.filters.setFilterValue(filterKey, nextValues);}return;}table.filters.clearFilterValue(filterKey);},[table.filters, table.columns],);return (<divstyle={{height: "500px",maxWidth: "1200px",}}>{withMaleFilterButton && (<div style={{display: "flex", gap: "8px"}}><ButtononClick={() => table.filters.setFilterValue("gender", ["Male"])}buttonStyle="flat"color="neutral">Set Male Filter</Button></div>)}<DataTable<User>{...table}onRowClick={(data, index) => console.log(data, index)}columns={columnsWithContextMenu}rows={rows}onFilterChipRemove={handleRemoveFilterChip}toolbar={dataTableToolbar}modalText={modalText}storageId={"data_table_example"}buttons={<Button onClick={addUser} buttonStyle="flat" color="primary" primaryIcon={<IconAdd />}>Add User</Button>}/></div>);};
Custom actions
Columns can hold action components instead of checkboxes, together with onRowClick.
Action
Name
Gender
Age
Birthday
Height
Weight
Eye Color
City
Origin
Marriage Status
Alive
"use client";import {Button,ButtonGroup,ColumnConfig,DataTable,DataTableChip,IconAdd,IconArrowDown,IconArrowUp,IconFileCopy,IconEdit,IconMore2Alt,IconThumbDown,IconThumbUp,IconDeleteBin5,Link,LinkProps,Menu,RowConfig,SimpleTableOptions,SplitButton,ToggleButton,useSimpleTable,useToolbar,} from "@brynlabs/fusion-ui";import React, {ReactNode, useCallback, useMemo, useState} from "react";export const extractTextFromElement = (obj: ReactNode): string => {if (typeof obj === "string") return obj;if (React.isValidElement(obj)) {// @ts-expect-error children are allowed to be undefinedreturn extractTextFromElement(obj.props.children);}if (Array.isArray(obj)) {return obj.map(e => extractTextFromElement(e)).join(" ");}return "";};export type User = {name: string | React.ReactElement<LinkProps>;gender: string;age: string | React.ReactElement<LinkProps>;birthday: string;height: string;weight: string;eyeColor: string;city: string | React.ReactElement<LinkProps>;origin: string;marriageStatus: string;alive: "Alive" | "Deceased";};type DataTableTestWrapperProps = {withMaleFilterButton?: boolean;};const data: User[] = [{name: "Max Müller",gender: "Male",age: "29",birthday: "1996-03-14",height: "182",weight: "78",eyeColor: "Blue",city: "Berlin",origin: "German",marriageStatus: "Single",alive: "Alive",},{name: "Sophia Schmidt",gender: "Female",age: "24",birthday: "2001-07-22",height: "168",weight: "60",eyeColor: "Green",city: "Berlin",origin: "German",marriageStatus: "Single",alive: "Alive",},{name: "Emma Johnson",gender: "Female",age: "31",birthday: "1994-11-30",height: "170",weight: "65",eyeColor: "Brown",city: (<Linkkey="newyork"link={{title: "New York City", href: "https://en.wikipedia.org/wiki/New_York_City"}}linkStyle="secondary">New York</Link>),origin: "American",marriageStatus: "Married",alive: "Alive",},{name: "Luca Rossi",gender: "Male",age: "27",birthday: "1998-05-18",height: "176",weight: "72",eyeColor: "Brown",city: "Rome",origin: "Italian",marriageStatus: "Widowed",alive: "Alive",},{name: "Marie Dubois",gender: "Female",age: "63",birthday: "1942-09-12",height: "165",weight: "58",eyeColor: "Blue",city: (<Linkkey="france"link={{title: "France", href: "https://en.wikipedia.org/wiki/France"}}linkStyle="secondary">French</Link>),origin: "French",marriageStatus: "Married",alive: "Deceased",},{name: (<Linkkey="jamesanderson"link={{title: "James Anderson",href: "https://en.wikipedia.org/wiki/James_Anderson_(cricketer)",}}linkStyle="secondary">James Anderson</Link>),gender: "Male",age: "35",birthday: "1990-01-09",height: "185",weight: "85",eyeColor: "Green",city: "New York",origin: "American",marriageStatus: "Divorced",alive: "Alive",},{name: "Anna Kowalski",gender: "Female",age: "29",birthday: "1996-06-25",height: "172",weight: "63",eyeColor: "Hazel",city: "Berlin",origin: "Polish",marriageStatus: "Single",alive: "Alive",},{name: "Carlos García",gender: "Male",age: "40",birthday: "1985-02-03",height: "180",weight: "82",eyeColor: "Brown",city: "Madrid",origin: "Spanish",marriageStatus: "Married",alive: "Alive",},{name: "Emily Clark",gender: "Female",age: "26",birthday: "1999-04-11",height: "169",weight: "59",eyeColor: "Blue",city: "Toronto",origin: "Canadian",marriageStatus: "Single",alive: "Alive",},{name: "Kenji Tanaka",gender: "Male",age: "32",birthday: "1993-08-19",height: "174",weight: "68",eyeColor: "Brown",city: "Tokyo",origin: "Japanese",marriageStatus: "Married",alive: "Alive",},{name: "Max Müller",gender: "Male",age: (<Linkkey="24"link={{title: "24", href: "https://en.wikipedia.org/wiki/24_(number)"}}linkStyle="secondary">24</Link>),birthday: "2001-01-10",height: "185",weight: "85",eyeColor: "Blue",city: "Hamburg",origin: "German",marriageStatus: "Single",alive: "Alive",},];const usersToAdd: User[] = [{name: "Olivia Brown",gender: "Female",age: "28",birthday: "1997-02-18",height: "171",weight: "62",eyeColor: "Brown",city: "London",origin: "British",marriageStatus: "Single",alive: "Alive",},{name: "Noah Wilson",gender: "Male",age: "34",birthday: "1991-10-05",height: "183",weight: "81",eyeColor: "Blue",city: "Dublin",origin: "Irish",marriageStatus: "Married",alive: "Alive",},{name: "Mia Svensson",gender: "Female",age: "30",birthday: "1995-06-12",height: "167",weight: "57",eyeColor: "Green",city: "Stockholm",origin: "Swedish",marriageStatus: "Single",alive: "Alive",},{name: "Ethan Novak",gender: "Male",age: "41",birthday: "1984-09-27",height: "179",weight: "76",eyeColor: "Hazel",city: "Prague",origin: "Czech",marriageStatus: "Divorced",alive: "Alive",},];const modalText = {errorMessages: {noDataFound: "No data was found",noDataFoundInSearch: "No searched results",noDataFoundInFilter: "No filtered results",noVisibleColumns: "No visible columns",},editView: {title: "Adjust columns",activeContainerTitle: "Visible Columns",inactiveContainerTitle: "Hidden Columns",activeContainerEmptyMessage: "No columns are visible",inactiveContainerEmptyMessage: "No columns are hidden",showAllButton: "Show all columns",applyButton: "Apply",abortViewText: {title: "Discard changes?",description:"You have unsaved changes. If you continue without saving, these changes will be lost.",confirmButton: "Discard",cancelButton: "Cancel",},},filterView: {title: "Adjust Filters",searchPlaceholder: "Type to search ...",min: "Minimum",max: "Maximum",applyButton: "Apply",resetButton: "Reset Filters",abortViewText: {title: "Discard changes?",description:"You have unsaved changes. If you continue without saving, these changes will be lost.",confirmButton: "Discard",cancelButton: "Cancel",},},};export const DataTableCustomButtonExample = ({withMaleFilterButton = false,}: DataTableTestWrapperProps) => {const [users, setUsers] = useState<User[]>(data);const [nextMockUserIndex, setNextMockUserIndex] = useState(0);const addUser = useCallback(() => {const nextUser = usersToAdd[nextMockUserIndex % usersToAdd.length];if (!nextUser) return;setUsers(prev => [...prev, nextUser]);setNextMockUserIndex(prev => prev + 1);}, [nextMockUserIndex]);const tableOptions = useMemo<SimpleTableOptions<User>>(() => {const columns: ColumnConfig<User>[] = [{key: "customAction",label: "Action",initialWidth: 120,isSortable: false,hasContextMenu: false,render: () => (<SplitButton><ToggleButtonactive={false}onChange={() => {console.log("Positive clicked");}}color="success"primaryIcon={<IconThumbUp />}/><ToggleButtonactive={false}onChange={() => {console.log("Negative clicked");}}color="critical"primaryIcon={<IconThumbDown />}/></SplitButton>),},{key: "name", label: "Name", render: row => row.name},{key: "gender",render: row => row.gender,label: "Gender",filter: {type: "multiSelect", getValue: row => row.gender},},{key: "age",render: row => row.age,label: "Age",sortFunction: (a, b) =>Number(extractTextFromElement(a.age)) - Number(extractTextFromElement(b.age)),filter: {type: "range", min: 0, max: 100, getValue: row => Number(row.age)},},{key: "birthday",render: row => row.birthday,label: "Birthday",filter: {type: "custom",filterFunction: (row, value) => {if (!value || typeof value !== "object" || Array.isArray(value)) return true;const typedValue = value as {from?: string; to?: string};if (typedValue.from && row.birthday < typedValue.from) return false;return !(typedValue.to && row.birthday > typedValue.to);},},},{key: "height", render: row => row.height, label: "Height"},{key: "weight", render: row => row.weight, label: "Weight"},{key: "eyeColor",render: row => row.eyeColor,label: "Eye Color",filter: {type: "multiSelect", getValue: row => row.eyeColor},},{key: "city",render: row => row.city,label: "City",filter: {type: "multiSelect", getValue: row => extractTextFromElement(row.city)},},{key: "origin",render: row => row.origin,label: "Origin",filter: {type: "multiSelect", getValue: row => row.origin},},{key: "marriageStatus",render: row => row.marriageStatus,label: "Marriage Status",filter: {type: "multiSelect", getValue: row => row.marriageStatus},},{key: "alive",render: row => row.alive,label: "Alive",filter: {type: "boolean",trueLabel: "Alive",falseLabel: "Deceased",getValue: row => row.alive === "Alive",},},];return {columns,searchIndexRow: row => columns.map(column => extractTextFromElement(column.render(row))),initialColumns: ["customAction","name","gender","age","birthday","height","weight","eyeColor","city","origin","marriageStatus","alive",],};}, []);const table = useSimpleTable<User>(users, tableOptions);const deleteRow = useCallback((rowIndex: number) => {setUsers(prev => prev.filter((_, index) => index !== rowIndex));}, []);const deleteSelectedRows = useCallback(() => {setUsers(prev => prev.filter((_, index) => !table.selectionBehavior.selection[index]));}, [table.selectionBehavior.selection]);const selectedCount = table.selectionBehavior.selectionSize;const getRowContextMenu = useCallback((rowIndex: number): Menu => [{items: [{title: "Duplicate",icon: <IconFileCopy />,callback: () => undefined,},{title: "Download file",icon: <IconArrowDown />,callback: () => undefined,},{title: "Delete",icon: <IconDeleteBin5 />,callback: () => deleteRow(rowIndex),},],},],[deleteRow],);const rows: RowConfig<User>[] = useMemo(() =>table.rows.map(item => ({...item,buttons: [{icon: <IconEdit />, action: () => undefined},{icon: <IconMore2Alt />, contextMenu: getRowContextMenu(item.index)},],})),[getRowContextMenu, table.rows],);const dataTableToolbarConfig = useMemo(() => ({segments: [{segment: [{section: `${selectedCount} selected`, hasDivider: true},{section: `Total: ${users.length}`},],},{segment: [{section: (<ButtonGroup><ButtonbuttonStyle="ghost"size="large"color="neutral"secondaryIcon={<IconArrowUp />}>Status</Button><ButtonbuttonStyle="ghost"size="large"color="neutral"secondaryIcon={<IconFileCopy />}>Duplicate</Button><ButtonbuttonStyle="ghost"size="large"color="neutral"secondaryIcon={<IconArrowDown />}>Export</Button></ButtonGroup>),hasDivider: true,hasSidePadding: false,},{section: (<ButtonbuttonStyle="flat"size="large"color="critical"primaryIcon={<IconDeleteBin5 />}onClick={deleteSelectedRows}>Delete</Button>),hasSidePadding: false,},],},],}),[deleteSelectedRows, selectedCount, users.length],);const dataTableToolbar = useToolbar(dataTableToolbarConfig);const columnsWithContextMenu: ColumnConfig<User>[] = useMemo(() => {const hiddenCategories = table.columns.filter(col => table.visibleColumns.findIndex(visible => col.key === visible) == -1,);return table.columns.map(column => ({...column,contextMenu:column.hasContextMenu === false? undefined: [{items: [{title: "Hide column",callback: () =>table.setVisibleColumns(prev =>prev.filter(prevKey => prevKey !== column.key),),},{icon: <IconAdd />,title: "Show column to the right",children:hiddenCategories.length > 0? hiddenCategories.map(category => ({title: category.label,callback: () =>table.setVisibleColumns(prev => {const clickedIndex = prev.findIndex(category => category === column.key,);if (clickedIndex === -1) return prev;return [...prev.slice(0, clickedIndex + 1),category.key,...prev.slice(clickedIndex + 1),];}),})): [{title: "No hidden columns",callback: () => undefined,},],},],},],}));}, [table]);const handleRemoveFilterChip = useCallback((chip: DataTableChip) => {const filterKey = chip.filterKey;const column = table.columns.find(column => String(column.key) === filterKey);if (!column?.filter) return;const currentValue = table.filters.values[filterKey];if (column.filter.type === "multiSelect") {const nextValues = (Array.isArray(currentValue) ? currentValue : []).filter(value => value !== chip.itemValue,);if (nextValues.length === 0) {table.filters.clearFilterValue(filterKey);} else {table.filters.setFilterValue(filterKey, nextValues);}return;}table.filters.clearFilterValue(filterKey);},[table.filters, table.columns],);return (<divstyle={{height: "500px",maxWidth: "1200px",}}>{withMaleFilterButton && (<div style={{display: "flex", gap: "8px"}}><ButtononClick={() => table.filters.setFilterValue("gender", ["Male"])}buttonStyle="flat"color="neutral">Set Male Filter</Button></div>)}<DataTable<User>{...table}hasCheckboxes={false}onRowClick={(data, index) => console.log(data, index)}columns={columnsWithContextMenu}rows={rows}onFilterChipRemove={handleRemoveFilterChip}toolbar={dataTableToolbar}modalText={modalText}storageId={"data_table_example"}buttons={<Button onClick={addUser} buttonStyle="flat" color="primary" primaryIcon={<IconAdd />}>Add User</Button>}/></div>);};
Server side data
useServerTable loads the visible pages lazily, here with React Query. Search and filters run on the server.
Update a company to write the saved row directly into React Query’s page cache.
Use the table filters to select departments or an age range. Results are filtered by the mock server. The age distribution is fetched separately from a mock histogram endpoint.
ID
Name
Email
Avatar
Job Title
Department
Firma
Street
City
Country
ZIP Code
Phone
Bio
Age
Created At
Actions
"use client";import {Button, DataTable, LoadPageRequest, Page, useServerTable} from "@brynlabs/fusion-ui";import {QueryObserver,queryOptions,useMutation,useQuery,useQueryClient,} from "@tanstack/react-query";import React, {useCallback} from "react";import {fetchPeople,fetchPeopleAgeHistogram,peopleDepartments,Person,updatePerson,} from "@/app/(docs)/components/data-table/mock-people-api";const peopleQueryKey = ["server-table-people"] as const;const peoplePageOptions = (request: LoadPageRequest) =>queryOptions({queryKey: [...peopleQueryKey, request],queryFn: () => fetchPeople(request),staleTime: Infinity,});const modalText = {errorMessages: {noDataFound: "No data was found",noDataFoundInSearch: "No searched results",noDataFoundInFilter: "No filtered results",noVisibleColumns: "No visible columns",},editView: {title: "Adjust columns",activeContainerTitle: "Visible Columns",inactiveContainerTitle: "Hidden Columns",activeContainerEmptyMessage: "No columns are visible",inactiveContainerEmptyMessage: "No columns are hidden",showAllButton: "Show all columns",applyButton: "Apply",abortViewText: {title: "Discard changes?",description:"You have unsaved changes. If you continue without saving, these changes will be lost.",confirmButton: "Discard",cancelButton: "Cancel",},},filterView: {title: "Adjust Filters",searchPlaceholder: "Type to search ...",min: "Minimum",max: "Maximum",applyButton: "Apply",resetButton: "Reset Filters",abortViewText: {title: "Discard changes?",description:"You have unsaved changes. If you continue without saving, these changes will be lost.",confirmButton: "Discard",cancelButton: "Cancel",},},};export const DataTableServerExample = () => {const queryClient = useQueryClient();const ageHistogram = useQuery({queryKey: ["server-table-people-age-histogram"],queryFn: fetchPeopleAgeHistogram,staleTime: Infinity,});const update = useMutation({mutationFn: (person: Person) =>updatePerson(person.id, {company: `${person.company} (updated)`}),onSuccess: person => {// Update every cached page containing this person, without reloading the table.queryClient.setQueriesData<Page<Person>>({queryKey: peopleQueryKey},page =>page && {...page,content: page.content.map(row => (row.id === person.id ? person : row)),},);},});const table = useServerTable<Person>({columns: [{key: "id", render: row => row.id, label: "ID"},{key: "name", render: row => row.name, label: "Name"},{key: "email", render: row => row.email, label: "Email"},{key: "avatar", render: row => row.avatar, label: "Avatar"},{key: "jobTitle", render: row => row.jobTitle, label: "Job Title"},{key: "department", render: row => row.department, label: "Department"},{key: "company",render: row => row.company,label: "Firma",},{key: "address.street", render: row => row.address.street, label: "Street"},{key: "address.city", render: row => row.address.city, label: "City"},{key: "address.country", render: row => row.address.country, label: "Country"},{key: "address.zip", render: row => row.address.zip, label: "ZIP Code"},{key: "phone", render: row => row.phone, label: "Phone"},{key: "bio", render: row => row.bio, label: "Bio"},{key: "age", render: row => row.age, label: "Age"},{key: "createdAt", render: row => row.createdAt, label: "Created At"},{key: "actions",label: "Actions",render: person => (<Button disabled={update.isPending} onClick={() => update.mutate(person)}>Update company</Button>),},],filters: [{key: "department",type: "multiSelect",options: peopleDepartments,getValue: row => row.department,},{key: "age",type: "range",min: ageHistogram.data?.min ?? 18,max: ageHistogram.data?.max ?? 80,histogram: ageHistogram.data?.histogram,showTicks: true,getValue: row => row.age,},],initialColumns: ["id","name","email","avatar","jobTitle","department","company","address.street","address.city","address.country","address.zip","phone","bio","age","createdAt","actions",],loadPage: useCallback(request => queryClient.fetchQuery(peoplePageOptions(request)),[queryClient],),subscribeToPage: useCallback((request, onPage) => {// Only visible pages have active observers and refetch on invalidation.const observer = new QueryObserver(queryClient, peoplePageOptions(request));const unsubscribe = observer.subscribe(result => {if (result.data) onPage(result.data);});// A fresh cached query may not emit on subscribe. Apply changes made while offscreen.const current = observer.getCurrentResult();if (current.data) onPage(current.data);return unsubscribe;},[queryClient],),});return (<><p>Update a company to write the saved row directly into React Query’s page cache.</p><p>Use the table filters to select departments or an age range. Results are filtered by themock server. The age distribution is fetched separately from a mock histogram endpoint.</p><ButtononClick={async () => {// Default invalidation refetches every observed page, including offscreen pages.// Only mark them stale; reload detaches old observers and loads the visible range.await queryClient.invalidateQueries({queryKey: peopleQueryKey, refetchType: "none"});await table.reload();}}>Reload from server</Button>{update.isError && <p>Could not update the company. Please try again.</p>}<divstyle={{height: "500px",maxWidth: "1200px",}}><DataTable<Person>{...table}modalText={modalText}storageId={"data_table_server_example"}/></div></>);};

