@ehfuse/mui-virtual-data-table

中文文档
1.11.4发布于 2026/11/03MITTypeScriptES Module近 30 天下载 5770

A high-performance virtual data table component for React with Material-UI integration

原始使用文档
文档目录15 节
  1. Features
  2. 주요 기능
  3. Installation
  4. Peer Dependencies
  5. 📚 Documentation
  6. Quick Start
  7. Basic Props Signature
  8. Column options (1.2.0)
  9. Page scroll (1.7.0)
  10. Column resize (1.8.0)
  11. Row reorder (1.9.0)
  12. Mobile list (1.6.0)
  13. License
  14. Contributing
  15. Support

MUI Virtual Data Table

A high-performance virtual data table component for React with Material-UI integration, featuring virtualized rendering, infinite scroll, keyboard navigation, and customizable styling.

React와 Material-UI를 위한 고성능 가상화 데이터 테이블 컴포넌트입니다. 가상화 렌더링, 무한 스크롤, 키보드 탐색, 커스터마이징 가능한 스타일링을 제공합니다.

Features

  • 🚀 High Performance: Virtualized rendering with react-virtuoso for smooth scrolling with large datasets
  • ♾️ Infinite Scroll: Built-in infinite scroll with automatic loading indicator
  • ⌨️ Keyboard Navigation: Full keyboard support (Arrow keys, PageUp/Down, Home/End)
  • 🎨 Material-UI Integration: Seamless integration with MUI components
  • 🦓 Zebra Striping: Optional alternating row colors with custom color support
  • 🔧 TypeScript Support: Full TypeScript support with comprehensive type definitions
  • 🔄 Sorting: Built-in column sorting functionality
  • 🎯 Customizable: Highly customizable columns, styling, and scrollbar options
  • 📊 Empty State: Built-in empty state handling with custom message
  • 🔍 Loading State: Smooth loading transitions with fade-out animation
  • 📏 Grouped Headers: Support for grouped column headers with dynamic height calculation
  • 🎨 Custom Scrollbar: Beautiful custom scrollbar with full customization options

주요 기능

  • 🚀 고성능: react-virtuoso를 사용한 가상화 렌더링으로 대용량 데이터셋에서도 부드러운 스크롤
  • ♾️ 무한 스크롤: 자동 로딩 인디케이터가 포함된 무한 스크롤 기능
  • ⌨️ 키보드 탐색: 완전한 키보드 지원 (방향키, PageUp/Down, Home/End)
  • 🎨 Material-UI 통합: MUI 컴포넌트와의 완벽한 통합
  • 🦓 얼룩말 줄무늬: 사용자 지정 색상을 지원하는 선택적 행 교차 색상
  • 🔧 TypeScript 지원: 포괄적인 타입 정의가 포함된 완전한 TypeScript 지원
  • 🔄 정렬: 내장된 컬럼 정렬 기능
  • 🎯 커스터마이징: 컬럼, 스타일링, 스크롤바 옵션의 높은 커스터마이징
  • 📊 빈 상태: 커스텀 메시지를 지원하는 내장된 빈 상태 처리
  • 🔍 로딩 상태: 페이드아웃 애니메이션이 적용된 부드러운 로딩 전환
  • 📏 그룹 헤더: 동적 높이 계산을 지원하는 그룹화된 컬럼 헤더
  • 🎨 커스텀 스크롤바: 완전한 커스터마이징 옵션을 갖춘 아름다운 커스텀 스크롤바

Installation

npm install @ehfuse/mui-virtual-data-table

Peer Dependencies

This package requires the following peer dependencies:

npm install react react-dom @mui/material react-virtuoso

📚 Documentation

For detailed API reference and usage examples:

Quick Start

import { VirtualDataTable } from "@ehfuse/mui-virtual-data-table";
import type { DataColumn } from "@ehfuse/mui-virtual-data-table";

interface User {
    id: number;
    name: string;
    email: string;
}

const columns: DataColumn<User>[] = [
    { id: "id", text: "ID", width: 80 },
    { id: "name", text: "Name", width: 200, sortable: true },
    { id: "email", text: "Email", width: 250 },
];

const data: User[] = [
    { id: 1, name: "John Doe", email: "[email protected]" },
    { id: 2, name: "Jane Smith", email: "[email protected]" },
    // ... more data
];

function App() {
    return (
        <VirtualDataTable
            data={data}
            columns={columns}
            totalCount={data.length}
            loading={false}
        />
    );
}

Basic Props Signature

<VirtualDataTable
    // Required
    data={T[]}                              // Data array
    columns={DataColumn<T>[]}               // Column definitions
    totalCount={number}                     // Total data count

    // Optional - Styling
    striped={boolean | string}              // Zebra striping (true: default gray (#f5f5f5), string: custom color, default: false)
    rowDivider={boolean}                    // Show row borders (default: true)
    rowHeight={number}                      // Fixed row height in px (default: 50) — rows keep this height even when showFooter fills a taller container
    columnHeight={number}                   // Header height in px (default: 56, auto 2x for grouped headers)
    showPaper={boolean}                     // Wrap in Paper component (default: true)
    paddingX={string | number}              // Horizontal padding (default: "1rem")
    paddingTop={string | number}            // Top padding (default: 0)
    paddingBottom={string | number}         // Bottom padding (default: 0)
    variant={"default" | "notion"}          // Table look. "notion": Notion-like (40px header, 13px grey header text, cell dividers, flat hover) — matches @ehfuse/taskbox tables
    rowHoverColor={string}                  // Row hover background color (default: "#000000", auto-inverted brightness in dark mode)
    rowHoverOpacity={number}                // Row hover opacity 0-1 (default: 0.06)
    viewportBuffer={number | { top, bottom }} // Viewport outside pre-render buffer in px (default: rowHeight-based auto buffer)

    // Optional - Infinite Scroll
    loading={boolean}                       // Loading state (default: false)
    onLoadMore={(offset, limit) => void}    // Load more callback (enables infinite scroll when provided)
    // ⚠️ onLoadMore 계약: 호출되면 반드시 이행해서 data 가 늘어나거나 loading 이 토글돼야 합니다.
    //    (정렬/검색/필터로 목록을 1페이지로 되돌리면 컴포넌트가 자동으로 더보기 잠금을 풀어 재개합니다.)

    // Optional - Sorting
    sortBy={string}                         // Current sort field
    sortDirection={"asc" | "desc"}          // Current sort direction
    onSort={(id, direction) => void}        // Sort callback

    // Optional - Interactions
    onRowClick={(item, index) => void}      // Row click handler

    // Optional - Customization
    emptyMessage={string | React.ReactNode} // Empty state message (default: "NO DATA")
    scrollbars={VDTOverlayScrollbarProps}   // Custom scrollbar options
    LoadingComponent={React.ComponentType}  // Custom loading component

    // Optional - Layout (1.2.0)
    framed={boolean}                        // notion: outer border + paper padding (default: true). false = fill an already-bordered box
    headerTopBorder={boolean}               // notion + framed=false: draw a top line on the header row (table shares a box with a toolbar above)
    tableLayout={"fixed" | "auto"}          // "auto": column widths follow content, wide tables scroll horizontally
    rowHeightMode={"fixed" | "auto"}        // "auto": rowHeight becomes a minimum, rows grow with content
    autoHeight={boolean}                    // No virtualization / no own scroll — the table is as tall as its rows (page scrolls)
    renderFullWidthRow={(item, index) => ReactNode | null} // Group band row: one cell spanning all columns (null = normal row)
    fullWidthRowSx={SxProps}                // Style of the band row cell (background, padding)
    onRowContextMenu={(item, index, event) => void}

    // Optional - 1.3.0
    pinnedRows={T[]}                        // Rows pinned under the header (always visible, same columns/cells/band rows)
    pinnedRowsMaxHeight={number}            // Max height of the pinned area in px — it scrolls on its own beyond that
    pinnedRowsSx={SxProps}                  // Pinned area style (default: 1px strong bottom border)
    resetScrollOnDataChange={boolean}       // false: keep the scroll position when rows are removed (collapse groups); remount with key to reset
    rowKeyFromId={boolean}                  // Use getRowId as the React key of rows (rows with inline editors)
    onEndReached={() => void}               // Fired every time the end is reached (no minimum / no same-length latch)
    colorMode={"light" | "dark"}            // notion colors independent of the MUI palette mode
    borderColor={string}                    // notion: color for every line (outer frame, header line, cell dividers). Default NOTION_LINE_COLOR (#d9d8d5, exported) — every notion line uses one color
    mobile={boolean | { query?, renderRow? }} // 1.6.0 card list on narrow screens (see "Mobile list")
    scrollParent={HTMLElement | null}         // 1.7.0 virtualize against an outer (page) scroller
    columnResize={boolean | { minWidth? }}    // 1.8.0 drag the header's right edge to resize a column (see "Column resize")
    columnWidths={Record<string, number>}     // 1.8.0 resized widths by column id (you store them)
    onColumnWidthsChange={(widths) => void}   // 1.8.0 fired once on release / on double-click reset
    rowReorder={{ onReorder, canDrag?, disabledReason?, handleWidth?, handleLabel? }} // 1.9.0 drag rows by a handle (see "Row reorder")

    // Optional - 1.4.0 header context menu (right-click on th never shows the browser menu)
    onSortClear={() => void}                // adds "정렬 풀기" to the header menu (and re-picking the active direction clears)
    onHeaderContextMenu={(column, event) => boolean | void} // custom header menu — return true or preventDefault() to skip the built-in one
/>

Column options (1.2.0)

{ id: "name", text: "이름", width: 160, pinned: "left" }     // sticky left (numeric width needed for the next pinned column)
{ id: "name", text: "이름", primary: true }                  // 15px body text for the row's main cell
{ id: "d1", text: "월", onCellClick: (item, i, e) => {} }    // per-cell click + cell hover (row click is not fired)
{ id: "who", text: "담당", rowSpan: (item, i) => 2 }         // merge down; return 0 on covered rows (use with autoHeight / short tables)
{ id: "m1", text: "1월", cellPadding: "4px 0" }              // body padding override
{ id: "t", text: "업무", cellStyle: (item) => ({ color: "#999" }) } // per-row cell style
{ id: "c", text: "열", headerProps: { draggable: true, onDrop } }  // extra th attributes (drag to reorder, title…)
{ id: "who", text: "책임자", width: 110, colSpan: (item) => (item.isEvent ? 2 : 1) } // 1.3.0: merge right (the covered column is not drawn)
{ id: "title", text: "제목", minWidth: 220 }                // 1.3.0: flexible column (no width) with a minimum — table min width = sum, scrolls sideways

Header context menu (1.4.0) — right-click a header cell:

// sort items appear for sortable columns when onSort is given
{ id: "name", text: "이름", sortable: true }
// filter checklist (multi by default; filterMultiple: false = pick one)
{
    id: "status", text: "상태",
    filterOptions: [{ value: "open", label: "진행" }, { value: "done", label: "완료" }],
    filterValues: status,                 // string[] — non-empty → header turns primary + filter icon
    onFilterChange: (values) => setStatus(values),
}
{ id: "memo", text: "메모", headerMenu: false } // no menu on this column (browser menu still blocked)
{ id: "due", text: <MyHeader />, sortMenu: true } // 1.4.1: sort items in the menu without VDT's sort label/click (custom-drawn header)

// open your own header menu instead
<VirtualDataTable onHeaderContextMenu={(col, e) => (col.id === "owner" ? (openOwnerMenu(e), true) : false)} … />

Shared header menu (1.5.0) — open it yourself from a custom header button / toolbar button:

import { HeaderMenu, headerMenuAnchor } from "@ehfuse/mui-virtual-data-table";

const [pos, setPos] = useState<{ left: number; top: number } | null>(null);
<button onClick={(e) => setPos(headerMenuAnchor(e.currentTarget))}>상태</button>
<HeaderMenu
    open={Boolean(pos)}
    anchorPosition={pos}
    onClose={() => setPos(null)}
    sort={{ active: sortDir, onChange: (d) => setSortDir(d) }}           // null = 풀기
    filter={{
        options: [{ value: "open", label: "진행", icon: <Dot color="green" /> }],
        value: picked, onChange: setPicked,
        multiple: true,                 // false = radio (closes on pick), allLabel: "전체"
        emptyLabel: "비어 있음",        // adds value ""
        searchable: undefined,          // default: > 10 options
    }}
>
    {/* extra items */}
</HeaderMenu>

Pinned rows example (1.3.0):

<VirtualDataTable
    variant="notion"
    data={rows}                                   // without the pinned ones
    pinnedRows={[{ kind: "band", label: "미확인" }, ...unconfirmed]}
    pinnedRowsMaxHeight={49 * 6}                  // band + 5 rows, then it scrolls
    renderFullWidthRow={(r) => (r.kind === "band" ? r.label : null)}
    getRowId={(r) => r.id}
    rowKeyFromId
    resetScrollOnDataChange={false}
    ...
/>

notion defaults (1.2.0): rowHeight 48, header text 13px, selected row (selectedRowId) = #dbeafe + 3px #3b82f6 left bar (dark values included), string emptyMessage = one light grey line under the header, footer cells use the body padding.

Page scroll (1.7.0)

scrollParent={element} virtualizes against an outer scroller (react-virtuoso customScrollParent) — the table has no scroller of its own and grows with the page, so things bound to the page scroller (collapsing app bar, scroll-to-top, pull-to-refresh) keep working. Pass null while the element is not known yet (nothing is drawn until then). Works with mobile and onEndReached.

Column resize (1.8.0)

columnResize puts a grip on each header cell's right edge (col-resize cursor, a thin line on hover). Drag it to resize that column, double-click it to go back to the column's own width. The table does not store anything — keep the widths where you like (account settings, localStorage …) and pass them back with columnWidths. Without columnWidths the table only remembers them until it remounts.

const [widths, setWidths] = useAccountSetting("customer.list"); // Record<columnId, px>
<VirtualDataTable
    columnResize                              // or { minWidth: 60 } — default minimum 48px (a column's own minWidth wins)
    columnWidths={isPhone ? undefined : widths}
    onColumnWidthsChange={setWidths}          // once on release; double-click → the key is removed
    ...
/>
  • While dragging only CSS variables on the table box change — rows are not re-rendered, virtualization and horizontal scroll stay as they are.
  • The click that follows a drag is swallowed, so releasing over a header never sorts / opens the header menu.
  • A flexible column (no width) keeps a minimum of its minWidth (or the resize minimum) once anything was resized; when every column ends up with a pixel width the table is as wide as their sum (no stretching that would make drawn widths differ from the dragged ones). If no column is flexible, the first drag captures every column's drawn width (1.8.1) so the other columns do not jump.
  • Works with pinned: "left" (sticky offsets follow), autoHeight, framed, tableLayout, variant="notion", pinnedRows, footer. In the mobile card list there is no grip and columnWidths is ignored.

Row reorder (1.9.0)

rowReorder adds a drag handle (⋮⋮) as the first cell. Drag it to move the row; a line shows where it will land and onReorder(from, to, item) fires on release (to is the index the row ends up at — the same as arrayMove(data, from, to)). The table never changes data; reorder it yourself and pass it back.

<VirtualDataTable
    rowReorder={{
        onReorder: (from, to) => setRows((r) => arrayMove(r, from, to)),
        disabledReason: sortBy ? "정렬을 풀면 옮길 수 있어요" : undefined, // handle is dimmed, drag is off
        canDrag: (row) => !row.locked,                                    // no handle on locked rows
    }}
    ...
/>
  • Nothing re-renders while dragging — the dragged row gets a data-vdt-dragging attribute (dimmed) and the drop line is a positioned element, so it stays light with virtualization and long lists. Dragging to the edge auto-scrolls (the table scroller, scrollParent, or the document).
  • Keyboard: focus the handle → Space (lift) → ↑ / ↓ (moves one step, onReorder each time) → Space / Enter / Esc (drop).
  • In the mobile card list the handle sits at the left of the card. With pinned: "left" columns the handle column is pinned too. Rows in pinnedRows cannot be moved. Works with autoHeight, scrollParent, variant="notion", columnResize.
  • Row click still works everywhere except on the handle; the click after a drag is not treated as a row click.

Mobile list (1.6.0)

mobile turns the table into a card list on narrow screens (default query (max-width:599.95px)): no header row, one card per row, virtualization / onLoadMore / onRowClick / selectedRowId work the same. Header menu, footer and pinned rows are not drawn there.

<VirtualDataTable
    mobile                                   // or { query: "(max-width:899.95px)", renderRow: (item, index) => <MyCard/> }
    columns={[
        { id: "thumb", text: "", mobile: "thumb", render: (r) => <img src={r.image} width={56} /> },
        { id: "title", text: "작품", mobile: "title" },
        { id: "status", text: "상태", mobile: "badge", render: (r) => <Chip label={r.status} /> },
        { id: "artist", text: "작가", mobile: "sub" },
        { id: "price", text: "판매가" },              // no hint → meta line "판매가 1,000,000원"
        { id: "memo", text: "메모", mobile: false },  // hidden on mobile
    ]}
    ...
/>

Slots: title (bold first line — default: the first column), thumb (left), badge (right of the title), sub (line under the title), meta ("header value" pairs that wrap — default for the other columns), false (hidden). mobileLabel overrides the meta label (false = value only).

License

MIT © KIM YOUNG JIN ([email protected])

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Support

For support, email [email protected] or create an issue on GitHub.

版本与发布记录

@ehfuse/mui-virtual-data-table 共发布 78 个版本,首次发布于 2025/10/01;过去 12 个月发布了 69 个版本。

版本发布日期距今
1.11.4latest2026/11/0310 天前
1.11.32026/11/0310 天前
1.11.22026/11/0310 天前
1.11.12026/11/0310 天前
1.11.02026/10/3014 天前
1.10.42026/10/2915 天前

关于 @ehfuse/mui-virtual-data-table 的常见问题

@ehfuse/mui-virtual-data-table 是什么?

A high-performance virtual data table component for React with Material-UI integration

如何安装 @ehfuse/mui-virtual-data-table?

在项目目录运行 npm install @ehfuse/mui-virtual-data-table,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(>=16.8.0)、react-dom(>=16.8.0)、@mui/material(>=5.0.0)、@emotion/react(>=11.0.0)、react-virtuoso(>=4.0.0)、@emotion/styled(>=11.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@ehfuse/mui-virtual-data-table 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@ehfuse/mui-virtual-data-table 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@ehfuse/mui-virtual-data-table 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=16.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@ehfuse/mui-virtual-data-table 可以商用吗?

@ehfuse/mui-virtual-data-table 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@ehfuse/mui-virtual-data-table 的下载量有多少?

根据 npm 官方下载统计,@ehfuse/mui-virtual-data-table 在 2026/10/13 – 2026/11/11 这 30 天内被下载 5,770 次,日均约 192 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@ehfuse/mui-virtual-data-table 还在维护吗?

最新版本 1.11.4 发布于 2026/11/03(10 天前);至今共发布 78 个版本,其中过去 12 个月发布了 69 个;首次发布于 2025/10/01。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。