Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(table): virtualization #4285

Open
wants to merge 12 commits into
base: canary
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/fresh-windows-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nextui-org/table": patch
---

Virtualization support added to Table component
3 changes: 2 additions & 1 deletion apps/docs/config/routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,8 @@
"key": "table",
"title": "Table",
"keywords": "table, data display, grid, spreadsheet",
"path": "/docs/components/table.mdx"
"path": "/docs/components/table.mdx",
"updated": true
},
{
"key": "tabs",
Expand Down
8 changes: 8 additions & 0 deletions apps/docs/content/components/table/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import asyncPagination from "./async-pagination";
import infinitePagination from "./infinite-pagination";
import useCase from "./use-case";
import customStyles from "./custom-styles";
import virtualization from "./virtualization";
import virtualizationCustomItemHeight from "./virtualization-custom-row-height";
import virtualizationCustomMaxTableHeight from "./virtualization-custom-max-table-height";
import virtualizationTenThousand from "./virtualization-ten-thousand";

export const tableContent = {
usage,
Expand All @@ -42,4 +46,8 @@ export const tableContent = {
infinitePagination,
useCase,
customStyles,
virtualization,
virtualizationCustomItemHeight,
virtualizationCustomMaxTableHeight,
virtualizationTenThousand,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {Table, TableBody, TableCell, TableColumn, TableHeader, TableRow} from "@nextui-org/react";

function generateRows(count) {
return Array.from({length: count}, (_, index) => ({
key: index.toString(),
name: `Item ${index + 1}`,
value: `Value ${index + 1}`,
}));
}
Comment on lines +3 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Extract generateRows to a shared utility file

This helper function is duplicated across multiple example files. Consider moving it to a shared utility file to improve maintainability.

Create a new file apps/docs/content/components/table/utils.js:

export function generateRows(count) {
  return Array.from({length: count}, (_, index) => ({
    key: index.toString(),
    name: `Item ${index + 1}`,
    value: `Value ${index + 1}`,
  }));
}

Then import it in each example file:

+import {generateRows} from "./utils";
-function generateRows(count) {
-  // ... remove duplicated function
-}


export default function App() {
const rows = generateRows(500);
const columns = [
{key: "name", label: "Name"},
{key: "value", label: "Value"},
];

return (
<Table
isVirtualized
aria-label="Example of virtualized table with a large dataset"
maxTableHeight={300}
rowHeight={40}
>
<TableHeader columns={columns}>
{(column) => <TableColumn key={column.key}>{column.label}</TableColumn>}
</TableHeader>
<TableBody items={rows}>
{(item) => (
<TableRow key={item.key}>
{(columnKey) => <TableCell>{item[columnKey]}</TableCell>}
</TableRow>
)}
</TableBody>
</Table>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import App from "./virtualization-custom-max-table-height.raw.jsx?raw";

const react = {
"/App.jsx": App,
};

export default {
...react,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {Table, TableBody, TableCell, TableColumn, TableHeader, TableRow} from "@nextui-org/react";

function generateRows(count) {
return Array.from({length: count}, (_, index) => ({
key: index.toString(),
name: `Item ${index + 1}`,
value: `Value ${index + 1}`,
}));
}

export default function App() {
const rows = generateRows(500);
const columns = [
{key: "name", label: "Name"},
{key: "value", label: "Value"},
];

return (
<Table
isVirtualized
aria-label="Example of virtualized table with a large dataset"
maxTableHeight={500}
rowHeight={70}
>
<TableHeader columns={columns}>
{(column) => <TableColumn key={column.key}>{column.label}</TableColumn>}
</TableHeader>
<TableBody items={rows}>
{(item) => (
<TableRow key={item.key}>
{(columnKey) => <TableCell>{item[columnKey]}</TableCell>}
</TableRow>
)}
</TableBody>
</Table>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import App from "./virtualization-custom-row-height.raw.jsx?raw";

const react = {
"/App.jsx": App,
};

export default {
...react,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {Table, TableBody, TableCell, TableColumn, TableHeader, TableRow} from "@nextui-org/react";

function generateRows(count) {
return Array.from({length: count}, (_, index) => ({
key: index.toString(),
name: `Item ${index + 1}`,
value: `Value ${index + 1}`,
}));
}
Comment on lines +3 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider memoizing row generation

The generateRows function creates a new array on every render. Consider memoizing the result to prevent unnecessary re-renders.

+import {useMemo} from "react";

 function generateRows(count) {
   return Array.from({length: count}, (_, index) => ({
     key: index.toString(),
     name: `Item ${index + 1}`,
     value: `Value ${index + 1}`,
   }));
 }

 export default function App() {
-  const rows = generateRows(10000);
+  const rows = useMemo(() => generateRows(10000), []);

Committable suggestion skipped: line range outside the PR's diff.


export default function App() {
const rows = generateRows(10000);
const columns = [
{key: "name", label: "Name"},
{key: "value", label: "Value"},
];

return (
<Table
isVirtualized
aria-label="Example of virtualized table with a large dataset"
maxTableHeight={500}
rowHeight={40}
>
<TableHeader columns={columns}>
{(column) => <TableColumn key={column.key}>{column.label}</TableColumn>}
</TableHeader>
<TableBody items={rows}>
{(item) => (
<TableRow key={item.key}>
{(columnKey) => <TableCell>{item[columnKey]}</TableCell>}
</TableRow>
)}
</TableBody>
</Table>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import App from "./virtualization-ten-thousand.raw.jsx?raw";

const react = {
"/App.jsx": App,
};

export default {
...react,
};
37 changes: 37 additions & 0 deletions apps/docs/content/components/table/virtualization.raw.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {Table, TableBody, TableCell, TableColumn, TableHeader, TableRow} from "@nextui-org/react";

function generateRows(count) {
return Array.from({length: count}, (_, index) => ({
key: index.toString(),
name: `Item ${index + 1}`,
value: `Value ${index + 1}`,
}));
}

export default function App() {
const rows = generateRows(500);
const columns = [
{key: "name", label: "Name"},
{key: "value", label: "Value"},
];

return (
<Table
isVirtualized
aria-label="Example of virtualized table with a large dataset"
maxTableHeight={500}
rowHeight={40}
>
Comment on lines +19 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling and loading states

The table implementation could benefit from additional error handling and loading states to improve user experience.

 <Table
   isVirtualized
   aria-label="Example of virtualized table with a large dataset"
   maxTableHeight={500}
   rowHeight={40}
+  loadingState={isLoading ? "loading" : "idle"}
+  loadingContent={<Spinner label="Loading..." />}
+  emptyContent={<div>No rows to display</div>}
 >

Committable suggestion skipped: line range outside the PR's diff.

<TableHeader columns={columns}>
{(column) => <TableColumn key={column.key}>{column.label}</TableColumn>}
</TableHeader>
<TableBody items={rows}>
{(item) => (
<TableRow key={item.key}>
{(columnKey) => <TableCell>{item[columnKey]}</TableCell>}
</TableRow>
)}
</TableBody>
Comment on lines +28 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add aria-rowindex for better screen reader support

When virtualizing table rows, it's important to maintain proper accessibility by indicating the absolute row position to screen readers.

 <TableBody items={rows}>
   {(item) => (
-    <TableRow key={item.key}>
+    <TableRow 
+      key={item.key}
+      aria-rowindex={parseInt(item.key) + 2} // +2 to account for header row and 1-based indexing
+    >
       {(columnKey) => <TableCell>{item[columnKey]}</TableCell>}
     </TableRow>
   )}
 </TableBody>

</Table>
);
}
9 changes: 9 additions & 0 deletions apps/docs/content/components/table/virtualization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import App from "./virtualization.raw.jsx?raw";

const react = {
"/App.jsx": App,
};

export default {
...react,
};
48 changes: 48 additions & 0 deletions apps/docs/content/docs/components/table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,36 @@ import { useAsyncList } from "@react-stately/data";
iframeSrc="/examples/table/infinite-pagination"
/>


### Virtualization

Table supports virtualization, which allows efficient rendering of large lists by only rendering items that are visible in the viewport. You can enable virtualization by setting the `isVirtualized` prop to `true`.

<CodeDemo
title="Virtualization"
files={tableContent.virtualization}
/>

> **Note**: The virtualization strategy is based on the [@tanstack/react-virtual](https://tanstack.com/virtual/latest) package, which provides efficient rendering of large lists by only rendering items that are visible in the viewport.

#### Ten Thousand Items

Here's an example of using virtualization with 10,000 items.

<CodeDemo title="Ten Thousand Items" files={tableContent.virtualizationTenThousand} />

#### Max Table Height

The `maxTableHeight` prop is used to set the maximum height of the table. This is required when using virtualization. By default, it's set to `600`.

<CodeDemo title="Max Table Height" files={tableContent.virtualizationCustomMaxTableHeight} />

#### Custom Row Height

The `rowHeight` prop is used to set the height of each row in the table. This is required when using virtualization. By default, it's set to `40`.

<CodeDemo title="Custom Row Height" files={tableContent.virtualizationCustomItemHeight} />

### Use Case Example

When creating a table, you usually need core functionalities like sorting, pagination, and filtering. In the
Expand Down Expand Up @@ -456,6 +486,24 @@ You can customize the `Table` component by passing custom Tailwind CSS classes t
type: "none | sm | md | lg",
description: "The shadow size of the table.",
default: "sm"
},
{
attribute: "maxTableHeight",
type: "number",
description: "The maximum height of the table in pixels. Required when using virtualization.",
default: 600
},
{
attribute: "rowHeight",
type: "number",
description: "The fixed height of each row item in pixels. Required when using virtualization.",
default: 40
},
{
attribute: "isVirtualized",
type: "boolean",
description: "Whether to enable virtualization. By default, it's enabled when the number of items exceeds 50.",
default: "undefined"
},
{
attribute: "hideHeader",
Expand Down
3 changes: 2 additions & 1 deletion packages/components/table/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
"@react-stately/table": "3.13.0",
"@react-stately/virtualizer": "4.2.0",
"@react-types/grid": "3.2.10",
"@react-types/table": "3.10.3"
"@react-types/table": "3.10.3",
"@tanstack/react-virtual": "^3.10.9"
},
"devDependencies": {
"@nextui-org/theme": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion packages/components/table/src/table-row-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface TableRowGroupProps extends HTMLNextUIProps<"thead"> {
classNames?: ValuesType["classNames"];
}

const TableRowGroup = forwardRef<"thead", TableRowGroupProps>((props, ref) => {
const TableRowGroup = forwardRef<any, TableRowGroupProps>((props, ref) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid using any for the ref type to maintain type safety

Changing the ref type from "thead" to any reduces type safety and may introduce bugs. Specify the correct HTML element type to ensure proper type-checking and autocomplete support.

Apply this diff to restore type safety:

- const TableRowGroup = forwardRef<any, TableRowGroupProps>((props, ref) => {
+ const TableRowGroup = forwardRef<HTMLTableSectionElement, TableRowGroupProps>((props, ref) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const TableRowGroup = forwardRef<any, TableRowGroupProps>((props, ref) => {
const TableRowGroup = forwardRef<HTMLTableSectionElement, TableRowGroupProps>((props, ref) => {

const {as, className, children, slots, classNames, ...otherProps} = props;

const Component = as || "thead";
Expand Down
23 changes: 22 additions & 1 deletion packages/components/table/src/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ import {Spacer} from "@nextui-org/spacer";
import {forwardRef} from "@nextui-org/system";

import {UseTableProps, useTable} from "./use-table";
import VirtualizedTable from "./virtualized-table";
import TableRowGroup from "./table-row-group";
import TableHeaderRow from "./table-header-row";
import TableColumnHeader from "./table-column-header";
import TableSelectAllCheckbox from "./table-select-all-checkbox";
import TableBody from "./table-body";

export interface TableProps extends Omit<UseTableProps, "isSelectable" | "isMultiSelectable"> {}
export interface TableProps<T = object>
extends Omit<UseTableProps<T>, "isSelectable" | "isMultiSelectable"> {
isVirtualized?: boolean;
rowHeight?: number;
maxTableHeight?: number;
}

const Table = forwardRef<"table", TableProps>((props, ref) => {
const {
Expand All @@ -30,6 +36,10 @@ const Table = forwardRef<"table", TableProps>((props, ref) => {
ref,
});

const {isVirtualized, rowHeight = 40, maxTableHeight = 600} = props;

const shouldVirtualize = values.collection.size > 50 || isVirtualized;

const Wrapper = useCallback(
({children}: {children: JSX.Element}) => {
if (removeWrapper) {
Expand All @@ -41,6 +51,17 @@ const Table = forwardRef<"table", TableProps>((props, ref) => {
[removeWrapper, getWrapperProps],
);

if (shouldVirtualize) {
return (
<VirtualizedTable
{...(props as TableProps)}
ref={ref}
maxTableHeight={maxTableHeight}
rowHeight={rowHeight}
/>
);
}

return (
<div {...getBaseProps()}>
{topContentPlacement === "outside" && topContent}
Expand Down
Loading
Loading