React
Is new design vision part implemented using new tokens?
React
Is new design vision part implemented using new tokens?
import { DataGrid, type DataGridState } from '@mews/b2b-ui';
import { NativeCalendarService } from '@optimus-web/core';
import { useDateFormatPattern } from '@mews/framework/form';
const initialState: Partial<DataGridState> = {
grouping: ['ledgerType'],
columnVisibility: {
code: false,
creator: false,
created: false,
consumed: false,
centerCode: false,
taxRateCode: false,
taxName: false,
service: false,
isCanceled: false,
},
columnPinning: { right: ['value'] },
};
<DataGrid
model="client"
columns={columns}
data={data ?? []}
enableRowSelection={true}
initialState={initialState}
onStateChange={handleStateChange}
maxHeight={600}
meta={{ calendarService: NativeCalendarService, dateFormatPattern }}
onRowsChange={handleRowsChange}
headerQuickFiltersSlot={<SomeComponent />}
headerActionsSlot={<AnotherComponent />}
fillAvailableHeight={{ minHeight: 400, bottomOffset: 96 }}
loading={isLoading}
/>
The DataGrid component is part of the @mews/b2b-ui package.
import {
DataGrid,
createColumnHelper,
ColumnFilterType,
// Filter Operations
StringFilterOperation,
NumberFilterOperation,
DateFilterOperation,
DateTimeFilterOperation,
MultiSelectFilterOperation,
BooleanFilterOperation,
// Types
type DataGridState,
type DataGridProps,
type ColumnDef,
type ColumnFilter,
type DataGridActionToolbarConfig,
} from '@mews/b2b-ui';
// For DateTime filtering, also import:
import { DateTimeService } from '@mews-framework/datetime';
import { NativeCalendarService, NativeTimeService } from '@optimus-web/core';
DataGrid supports two distinct data models via discriminated union types.
All data is loaded upfront and operations (sorting, filtering, grouping) are computed in the browser. Best for small to medium datasets (hundreds to a few thousand rows).
<DataGrid
model="client"
data={myData}
columns={columns}
/>
Props specific to client-side:
Server-Side Model
Data is fetched from the server with operations applied server-side. Best for large datasets where loading all data upfront would impact performance.
<DataGrid
model="server"
useData={(state) => {
// state contains sorting, filtering, pagination info
const { data, totalRowCount } = useMyServerQuery(state);
return { data, totalRowCount };
}}
columns={columns}
columnUniqueValues={{
status: ['Active', 'Inactive', 'Pending'],
category: ['Electronics', 'Clothing', 'Food'],
}}
/>
Props specific to server-side:
|
Prop |
Type |
Default |
Description |
|---|---|---|---|
|
columns* |
ColumnDef<TData>[] |
- |
Column definitions (TanStack Table format) |
|
title |
ReactNode |
- |
Table title displayed in header |
|
subTitle |
ReactNode |
- |
Subtitle below the title |
|
defaultColumn |
Partial<ColumnDef<TData>> |
- |
Default values applied to all columns |
|
initialState |
Partial<DataGridState> |
- |
Initial state (sorting, filters, grouping, etc.) |
|
onStateChange |
(state: DataGridState) => void |
- |
Callback fired when any state changes |
|
onRowsChange |
(rows: RowModel<TData>) => void |
- |
Callback fired when visible rows change |
|
getRowId |
(row, index, parent?) => string |
- |
Custom function to generate row IDs |
|
getRowCanExpand |
(row: Row<TData>) => boolean |
- |
Controls whether a row can be expanded. By default, rows with subRows can expand. |
|
getRowIsVisible |
(row: Row<TData>) => boolean |
- |
Controls whether a row is rendered. Hidden rows are excluded from the DOM. |
Props to enable/disable DataGrid features. All default to true unless otherwise noted.
|
Prop |
Type |
Default |
Description |
|---|---|---|---|
|
enableFilters |
boolean |
true |
Enable column filtering UI |
|
enableGlobalFilter |
boolean |
true |
Enable global search input |
|
enableSorting |
boolean |
true |
Enable column sorting |
|
enableGrouping |
boolean |
true |
Enable column grouping |
|
enableExpanding |
boolean |
true |
Enable row expanding (for grouped rows) |
|
enableRowSelection |
boolean |
true |
Enable row selection checkboxes |
|
enableColumnPinning |
boolean |
true |
Callback fired when visible rows change |
|
enableColumnResizing |
boolean |
true |
Custom function to generate row IDs |
|
enableColumnOrdering |
boolean |
true |
Enable column drag-and-drop reordering |
|
enableHiding |
boolean |
true |
Enable column visibility toggle |
|
autoResetExpanded |
boolean |
false |
Auto-reset expanded state when data changes |
|
showGroupRowCount |
boolean | (row) => number | null |
false |
Show row count in grouped headers. Pass a function to customize per group. |
|
Prop |
Type |
Default |
Description |
|---|---|---|---|
|
emptyState |
EmptyStateProps |
- |
Configuration for empty state display |
|
virtualized |
boolean |
- |
Enable row virtualization for large datasets |
|
minHeight |
CSSProperties['minHeight'] |
- |
Minimum height of the DataGrid |
|
maxHeight |
CSSProperties['maxHeight'] |
- |
Maximum height (enables vertical scroll) |
|
fillAvailableHeight |
boolean |
- |
Fill available vertical space |
|
loading |
boolean |
- |
Show loading state |
|
error |
boolean |
- |
Show error state |
|
meta |
TableMeta |
- |
Metadata for date/time services (see Meta Configuration) |
|
actionToolbar |
DataGridActionToolbarConfig<TData> |
- |
Bulk action toolbar configuration |
|
headerQuickFiltersSlot |
ReactNode |
- |
Slot for quick filter controls in header |
|
headerActionsSlot |
ReactNode |
- |
Slot for 1-3 action buttons in header |
The state object that tracks all DataGrid operations. This is passed to onStateChange and used in initialState.
interface DataGridState {
pagination: {
pageSize: number; // Rows per page
pageIndex: number; // Current page (0-indexed)
};
sorting: SortingState; // Array<{ id: string; desc: boolean }>
columnFilters: Array<{
id: string; // Column ID
value: ColumnFilter[]; // Array of filters for this column
}>;
globalFilter: string; // Global search text
grouping: GroupingState; // string[] - Column IDs to group by
expanded: ExpandedState; // true | Record<string, boolean>
columnVisibility: VisibilityState; // Record<columnId, boolean>
columnOrder: ColumnOrderState; // string[] - Column order
columnPinning: ColumnPinningState; // { left?: string[]; right?: string[] }
columnSizing: ColumnSizingState; // Record<columnId, number>
rowSelection: RowSelectionState; // Record<rowId, boolean>
}
Use createColumnHelper for type-safe column definitions. DataGrid uses TanStack Table's column definition format.
const columnHelper = createColumnHelper<Person>();
const columns = [
columnHelper.accessor('firstName', {
header: () => 'First Name',
cell: ({ getValue }) => getValue(),
}),
];
columnHelper.accessor('status', {
header: () => 'Status',
meta: {
filterType: ColumnFilterType.MultiSelect,
},
}),
columnHelper.accessor('price', {
header: () => 'Price',
cell: ({ getValue }) => `$${getValue().toFixed(2)}`,
meta: {
filterType: ColumnFilterType.Number,
align: 'end',
width: 120,
},
}),
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: ({ row }) => <ActionsMenu row={row.original} />,
enableSorting: false,
enableHiding: false,
enablePinning: false,
enableResizing: false,
}),
columnHelper.accessor('amount', {
header: () => 'Amount',
aggregationFn: 'sum', // Built-in: sum, min, max, mean, count, median, unique, uniqueCount, extent
aggregatedCell: ({ getValue }) => <strong>${getValue().toFixed(2)}</strong>,
enableGrouping: false, // Prevent grouping by this column
footer: ({ table }) => {
// Custom footer calculation
const total = table.getRowModel().rows.reduce(
(sum, row) => sum + (row.getValue('amount') as number),
0
);
return <strong>Total: ${total.toFixed(2)}</strong>;
},
}),
For columns containing arrays, you must provide getUniqueValues to flatten values for the filter dropdown:
columnHelper.accessor('tags', {
header: () => 'Tags',
cell: ({ getValue }) => getValue().join(', '),
meta: { filterType: ColumnFilterType.MultiSelect },
// REQUIRED for array values - extracts individual values for filter options
getUniqueValues: (row) => row.tags,
}),
Use filterSortFn to customize how MultiSelect filter options are sorted:
const PRIORITY_ORDER = ['Critical', 'High', 'Medium', 'Low'];
columnHelper.accessor('priority', {
header: () => 'Priority',
meta: {
filterType: ColumnFilterType.MultiSelect,
// Custom sort: priority order first, then alphabetical
filterSortFn: (a, b) => {
const aIndex = PRIORITY_ORDER.indexOf(a);
const bIndex = PRIORITY_ORDER.indexOf(b);
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
if (aIndex !== -1) return -1;
if (bIndex !== -1) return 1;
return a.localeCompare(b);
},
},
}),
|
Option |
Type |
Description |
|---|---|---|
|
filterType |
ColumnFilterType |
Filter type: String, Number, Date, DateTime, MultiSelect, Boolean |
|
filterSortFn |
(a: string, b: string) => number |
Custom sort function for MultiSelect filter options |
|
globalFilterValue |
(value: unknown) => string |
Transform cell value for global search matching |
|
width |
number |
Fixed column width in pixels |
|
align |
start | end |
Both header and cell alignment |
|
headerAlign |
start | end |
Header alignment (overrides align) |
|
cellAlign |
start | end |
Cell alignment (overrides align) |
|
Option |
Type |
Description |
|---|---|---|
|
enableSorting |
boolean |
Enable/disable sorting for this column |
|
enableHiding |
boolean |
Enable/disable hiding for this column |
|
enablePinning |
boolean |
Enable/disable pinning for this column |
|
enableResizing |
boolean |
Enable/disable resizing for this column |
|
enableGrouping |
boolean |
Enable/disable grouping by this column |
DataGrid supports six filter types, each with specific operations. Filters now include a type discriminator field.
meta: { filterType: ColumnFilterType.String }
Operations (StringFilterOperation):
meta: { filterType: ColumnFilterType.Number }
Operations (NumberFilterOperation):
meta: { filterType: ColumnFilterType.Date }
Operations (DateFilterOperation):
Requirements: Requires meta.calendarService on the DataGrid.
meta: { filterType: ColumnFilterType.DateTime }
Operations (DateTimeFilterOperation):
Requirements:
Requires all of the following in DataGrid meta:
meta: { filterType: ColumnFilterType.MultiSelect }
Operations (MultiSelectFilterOperation):
Note: For columns containing arrays (e.g., tags), you must provide getUniqueValues on the column definition to flatten values for the filter dropdown:
getUniqueValues: (row) => row.tags
meta: { filterType: ColumnFilterType.Boolean }
Operations (BooleanFilterOperation):
The meta prop provides services required for date/time filtering and formatting.
import { NativeCalendarService, NativeTimeService } from '@optimus-web/core';
import { DateTimeService } from '@mews-framework/datetime';
<DataGrid
// ...other props
meta={{
// Required for Date filter
calendarService: NativeCalendarService,
// Required for DateTime filter
timeService: NativeTimeService,
dateTimeService: DateTimeService,
timezone: 'Europe/Prague',
// Optional: custom date format pattern
dateFormatPattern: 'MM/dd/yyyy',
}}
/>
|
Property |
Type |
Required For |
Description |
|---|---|---|---|
|
calendarService |
CalendarService Date |
DateTime filters |
Service for date operations |
|
timeService |
TimeService |
DateTime filter |
Service for time operations |
|
dateTimeService |
typeof DateTimeService |
DateTime filter |
Service from @mews-framework/datetime |
|
timezone |
string |
DateTime filter |
IANA timezone (e.g., 'Europe/Prague', 'America/New_York') |
|
dateFormatPattern |
string |
Optional |
Date format pattern (e.g., 'MM/dd/yyyy') |
initialState={{
sorting: [{ id: 'createdAt', desc: true }],
}}
initialState={{
columnFilters: [
{
id: 'status',
value: [{ type: 'multiSelect', operation: 'in', value: ['active', 'pending'] }]
},
{
id: 'amount',
value: [{ type: 'number', operation: 'greaterThan', value: 100 }]
},
],
}}
initialState={{
grouping: ['category', 'subcategory'],
expanded: true, // Expand all groups by default
// or expand specific rows:
// expanded: { 'category:Electronics': true, 'category:Clothing': false },
}}
initialState={{
columnPinning: {
left: ['name'], // Pin to left
right: ['actions'], // Pin to right
},
}}
initialState={{
columnVisibility: {
internalId: false, // Hidden by default
debugInfo: false,
},
}}
initialState={{
columnOrder: ['name', 'status', 'amount', 'createdAt', 'actions'],
}}
initialState={{
pagination: {
pageIndex: 0,
pageSize: 25,
},
}}
DataGrid provides two callbacks for controlling row behavior: getRowCanExpand and getRowIsVisible.
Controls whether a row's expand/collapse toggle is shown. By default, any row with subRows can be expanded.
// Only allow expansion for groups with more than 2 items
<DataGrid
getRowCanExpand={(row) => (row.subRows?.length ?? 0) > 2}
// ...other props
/>
Use cases:
Controls whether a row is rendered in the DOM. Hidden rows are completely excluded—they don't appear in the UI and don't participate in keyboard navigation.
// Only show rows where status is enabled
<DataGrid
getRowIsVisible={(row) => row.original.status === true}
// ...other props
/>
Use cases:
Aligning getRowIsVisible with enableRowSelection
Important: When using getRowIsVisible with row selection, ensure hidden rows cannot be selected. Otherwise, users may unknowingly have hidden rows in their selection.
// ❌ Bad: Hidden rows can still be selected
<DataGrid
getRowIsVisible={(row) => row.original.status === true}
enableRowSelection={true}
/>
// ✅ Good: Selection logic matches visibility logic
const isRowEnabled = (row) => row.original.status === true;
<DataGrid
getRowIsVisible={isRowEnabled}
enableRowSelection={isRowEnabled}
/>
This ensures that:
Configure bulk actions for selected rows using the floating action toolbar.
import { ActionToolbarButton } from '@optimus-web/core';
import { IconName } from '@optimus-web/icons';
<DataGrid
enableRowSelection
actionToolbar={{
actions: (table) => (
<>
<ActionToolbarButton
icon={IconName.Edit}
onClick={() => {
const selectedRows = table.getSelectedRows().rows;
handleEdit(selectedRows.map(row => row.original));
}}
type="button"
accessibleText="Edit selected"
>
Edit
</ActionToolbarButton>
<ActionToolbarButton
icon={IconName.Delete}
onClick={() => {
const selectedRows = table.getSelectedRows().rows;
handleDelete(selectedRows.map(row => row.original));
}}
type="button"
accessibleText="Delete selected"
>
Delete
</ActionToolbarButton>
</>
),
}}
/>
The table parameter provides read-only access to selection state:
|
Method |
Return Type |
Description |
|---|---|---|
|
getSelectedRows() |
RowModel<TData> |
Get all selected rows (filtered, leaf rows only) |
|
getVisibleRows() |
RowModel<TData> |
Get all visible (filtered) rows |
|
isSomeSelected() |
boolean |
Check if some (but not all) rows are selected |
|
isAllSelected() |
boolean |
Check if all visible rows are selected |
import { isAtLeastTablet, useBreakpointRange } from '@optimus-web/breakpoints';
const ActionToolbarActions = ({ table }) => {
const breakpointRange = useBreakpointRange();
const isCompact = !isAtLeastTablet(breakpointRange);
return (
<ActionToolbarButton
icon={IconName.Edit}
onClick={() => handleEdit(table.getSelectedRows().rows)}
type="button"
accessibleText="Edit"
>
{!isCompact && 'Edit'} {/* Hide label on mobile */}
</ActionToolbarButton>
);
};
Persist DataGrid state in URL for shareable links using the useDataGridUrlState hook.
import { useDataGridUrlState } from '@mews-commander/utils';
const MyComponent = () => {
const [initialState, handleStateChange] = useDataGridUrlState('myDataGrid', {
defaultState: {
sorting: [{ id: 'createdAt', desc: true }],
grouping: ['category'],
columnPinning: { right: ['actions'] },
},
});
return (
<DataGrid
model="client"
data={data}
columns={columns}
initialState={initialState}
onStateChange={handleStateChange}
/>
);
};
The following state keys are synchronized to the URL:
Note: pagination, expanded, columnSizing, and rowSelection are NOT synced to URL.
For grids that need user-saveable views on top of URL state synchronization, use useDataGridViews. It wraps useDataGridUrlState and adds CRUD over named views, an "active view" pointer, and an hasUnsavedChanges flag. URL stays the source of truth for grid state; views are snapshots of that state, persisted to localStorage by default in this V1.
import { useRef } from 'react';
import { DataGrid, DataGridHandle, DataGridViewsConfig } from '@mews/b2b-ui';
import { useDataGridViews } from '@mews-commander/utils';
const DEFAULT_STATE = {
sorting: [{ id: 'isoDate', desc: false }],
grouping: ['isoDate'],
columnPinning: { right: ['price', 'actions'] },
};
const MyListView = () => {
const dataGridRef = useRef<DataGridHandle>(null);
const {
dataGridState,
handleStateChange,
views,
activeViewId,
hasUnsavedChanges,
saveView,
updateView,
deleteView,
applyView,
duplicateView,
} = useDataGridViews('myGrid', { dataGridRef });
const viewsConfig: DataGridViewsConfig = {
items: views,
activeViewId,
hasUnsavedChanges,
onApply: applyView,
onSaveNew: saveView,
onSaveChanges: () => {
if (activeViewId) {
// Merge defaults so the saved snapshot includes baseline state
updateView(activeViewId, { state: { ...DEFAULT_STATE, ...dataGridState } });
}
},
onDelete: deleteView,
onRename: (viewId, newName) => updateView(viewId, { name: newName }),
onDuplicate: duplicateView,
};
return (
<DataGrid
ref={dataGridRef}
model="client"
data={data}
columns={columns}
initialState={{ ...DEFAULT_STATE, ...dataGridState }}
onStateChange={handleStateChange}
views={viewsConfig}
/>
);
};
On first render, useDataGridViews reconciles URL ↔ stored active view:
|
URL state on mount |
Behavior |
|---|---|
|
Empty |
Apply stored activeViewId, or defaultActiveViewId`if provided. |
|
Matches a saved view |
Mark that view active. |
|
Custom (no match) |
Keep URL state; restore stored activeViewId as "active with unsaved changes". |
This sync runs once at mount. After that, all changes are user-driven (apply, save, delete, etc.).
|
Argument |
Type |
Description |
|---|---|---|
|
gridId |
string |
Required. Used as both the URL search-param key and the localStorage namespace (dataGrid.views.${gridId}, dataGrid.activeView.${gridId}). Must be unique per grid. |
|
options.dataGridRef |
RefObject<DataGridHandle | null> |
Required. Used to imperatively apply view state to the mounted grid. |
|
options.defaultActiveViewId |
string | null |
Optional. Auto-applied on mount if URL is empty and no activeViewId is stored. Default: null |
|
options.storage |
ViewsStorage |
Optional. Custom storage adapter (e.g. backend-backed). Default: localStorageAdapter. |
|
options.enabled |
boolean |
Optional. When false, disables view CRUD and sync entirely (URL state still works). Use to gate behind a feature flag. Must be stable from first render — toggling false → true after mount won't trigger hydration. Default: true. |
|
Name |
Type |
Description |
|---|---|---|
|
dataGridState |
Partial<DataGridState> |
Live state synced with URL. Pass to `<DataGrid initialState>` (merge with your defaults). |
|
handleStateChange |
(state: Partial<DataGridState>) => void |
Pass to `<DataGrid onStateChange>`. Writes syncable keys to the URL. |
|
views |
DataGridView[] |
All saved views for this `gridId`. |
|
activeViewId |
string | null |
ID of the view the user is currently working from, or `null`. |
|
hasUnsavedChanges |
boolean |
true when current URL state differs from the active view's stored state. |
|
saveView |
(name: string) => void |
Snapshot current state as a new view and mark it active. Wire to onSaveNew. |
|
updateView |
(id: string, updates: Omit<Partial<DataGridView>, 'id'>) => void |
Patch a view's name and/or state. No-op for readOnly views. Use for both "save changes" and "rename". |
|
deleteView |
(id: string) => void |
Remove a view. No-op for readOnly views. Clears active view if it was the deleted one. |
|
applyView |
(id: string) => void |
Apply a view's state to the grid and mark it active. Wire to onApply. |
|
duplicateView |
(id: string) => void |
Clone a view with a localized (copy) / (copy N) suffix, mark it active, and apply it. Wire to onDuplicate. |
The hook return shape does not map 1-to-1 to DataGridViewsConfig. Wire it like this:
|
DataGridViewsConfig prop |
Source |
|---|---|
|
items |
views |
|
activeViewId |
activeViewId |
|
hasUnsavedChanges |
hasUnsavedChanges |
|
onApply |
applyView |
|
onSaveNew |
saveView |
|
onSaveChanges |
() => updateView(activeViewId, { state: { ...DEFAULTS, ...dataGridState } }) |
|
onDelete |
deleteView |
|
onRename |
(id, name) => updateView(id, { name }) |
|
onDuplicate |
duplicateView |
|
onCopyLink |
Optional. Defaults to copying window.location.href. |
Note: onSaveChanges should merge your default state into the snapshot so the saved view contains the full baseline (defaults + user changes), not just the user diff.
Set readOnly: true on a DataGridView to lock it. updateView and deleteView become no-ops for that view. Useful for system-provided defaults you ship with the app.
Replace localStorage with any persistence layer (e.g. backend API) by passing a custom adapter:
import type { ViewsStorage } from '@mews-commander/utils';
const backendStorage: ViewsStorage = {
getViews: (gridId) => /* … */,
saveView: (gridId, view) => /* … */,
updateView: (gridId, id, updates) => /* … */,
deleteView: (gridId, id) => /* … */,
getActiveViewId: (gridId) => /* … */,
setActiveViewId: (gridId, id) => /* … */,
};
useDataGridViews('myGrid', { dataGridRef, storage: backendStorage });
The default localStorageAdapter validates stored payloads with Zod and self-heals corrupted entries by clearing them.
Views (and the URL) only persist:
Note: pagination, expanded, columnSizing, and rowSelection are intentionally excluded — they are session-specific or too volatile to persist.
columnHelper.accessor('amount', {
header: () => 'Amount',
cell: ({ getValue }) => `$${getValue().toFixed(2)}`,
footer: ({ table }) => {
const rows = table.getRowModel().rows;
const total = rows.reduce((sum, row) => {
const amount = row.getValue('amount');
return sum + (typeof amount === 'number' ? amount : 0);
}, 0);
return (
<Stack vertical spacing="25" itemAlignment="end">
<Typography textStyle="bodyMediumStrong">Total</Typography>
<Typography textStyle="bodyLargeStrong">${total.toFixed(2)}</Typography>
</Stack>
);
},
meta: { filterType: ColumnFilterType.Number, align: 'end' },
}),
columnHelper.accessor('revenue', {
header: () => 'Revenue',
// Built-in aggregation functions: sum, min, max, mean, median, unique, uniqueCount, count, extent
aggregationFn: 'sum',
// Custom rendering for aggregated (grouped) cells
aggregatedCell: ({ getValue }) => (
<Typography textStyle="bodyMediumStrong">
${getValue<number>().toFixed(2)}
</Typography>
),
cell: ({ getValue }) => `$${getValue().toFixed(2)}`,
meta: { align: 'end' },
}),
columnHelper.accessor('scores', {
header: () => 'Avg Score',
aggregationFn: (columnId, leafRows, childRows) => {
const values = leafRows.map(row => row.getValue(columnId) as number);
const sum = values.reduce((a, b) => a + b, 0);
return values.length > 0 ? sum / values.length : 0;
},
aggregatedCell: ({ getValue }) => getValue<number>().toFixed(1),
}),
DataGrid replaces the deprecated Table component. Key differences:
|
Aspect |
Table (Deprecated) |
DataGrid |
|---|---|---|
|
Data model |
Server-side only |
Client + Server |
|
State management |
useTableState hook |
DataGridState + onStateChange |
|
Filters |
External implementation |
Built-in with 6 filter types |
|
Grouping |
Not supported |
Built-in with aggregation |
|
Column pinning |
fixedFirstColumn prop |
enableColumnPinning + initialState.columnPinning |
|
Global search |
External via TableHeader |
Built-in enableGlobalFilter |
|
Row selection |
useRowSelection hook |
Built-in enableRowSelection |
|
Bulk actions |
TableBulkActions component |
Built-in actionToolbar prop |
DataGrid virtualizes by default. Three props let you optimize for your use case:
// Small datasets: skip virtualization overhead for <50 rows
<DataGrid virtualizationThreshold={50} />
// Consistent tall rows (e.g., 80px)
<DataGrid estimatedRowHeight={80} />
// Variable height rows (multi-line content)
<DataGrid enableDynamicRowHeight />
When to use each: