Recommended patterns and anti-patterns.
Follow these best practices to build performant, maintainable, and accessible data grids. These guidelines cover performance optimization, code organization, accessibility, and common pitfalls.
Always memoize column definitions to prevent unnecessary re-renders:
// ✓ Good: Memoized columns
const columns = useMemo<GridColumnDef<User>[]>(() => [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
], []);
// ✗ Bad: Recreated every render
const columns = [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
];Use useCallback for event handlers:
// ✓ Good: Memoized callback
const handleRowClick = useCallback(
(row: User) => {
router.push(`/users/${row.id}`);
},
[router]
);
// ✗ Bad: New function every render
const handleRowClick = (row: User) => {
router.push(`/users/${row.id}`);
};// ✓ Good: Reusable memoized component
const UserCell = memo<{ user: User }>(({ user }) => (
<div>{user.name}</div>
));
{
cell: ({ row }) => <UserCell user={row.original} />,
}
// ✗ Bad: Inline function
{
cell: ({ row }) => <div>{row.original.name}</div>,
}// ✓ Good: Virtualization enabled for 1000+ rows
<ActiveGrid
data={largeDataset}
enableVirtualization={true}
{...}
/>
// ✗ Bad: No virtualization for 10,000 rows
<ActiveGrid
data={veryLargeDataset}
{...}
/>// ✓ Good: Server mode for large datasets
<ActiveGrid
mode="server"
fetchFn={fetchData}
data={[]} // Server mode manages data
{...}
/>
// ✗ Bad: Client mode with 100,000 rows
<ActiveGrid
mode="client"
data={massiveDataset}
{...}
/>Create reusable column configurations:
const columnTypes: Record<string, Partial<GridColumnDef<any>>> = {
currency: {
meta: {
cellAlign: 'right',
valueFormatter: (value) => `$${value.toFixed(2)}`,
filterType: 'number',
},
},
date: {
meta: {
valueFormatter: (value) => format(new Date(value), 'PP'),
filterType: 'date',
},
},
percentage: {
meta: {
cellAlign: 'right',
valueFormatter: (value) => `${value}%`,
},
},
};
// Use in columns
const columns: GridColumnDef<Product>[] = [
{
accessorKey: 'price',
header: 'Price',
...columnTypes.currency,
},
{
accessorKey: 'discount',
header: 'Discount',
...columnTypes.percentage,
},
];Set defaults for all columns:
<ActiveGrid
columnDefaults={{
size: 150,
minSize: 80,
maxSize: 400,
enableSorting: true,
enableResizing: true,
}}
columns={columns}
{...}
/>// ✓ Good: Extracted function
const getStatusVariant = (status: string) => {
switch (status) {
case 'active': return 'default';
case 'pending': return 'secondary';
default: return 'outline';
}
};
{
cell: ({ getValue }) => (
<Badge variant={getStatusVariant(getValue() as string)}>
{getValue()}
</Badge>
),
}
// ✗ Bad: Inline logic
{
cell: ({ getValue }) => {
const status = getValue() as string;
const variant =
status === 'active' ? 'default' :
status === 'pending' ? 'secondary' :
'outline';
return <Badge variant={variant}>{status}</Badge>;
},
}Configure defaults app-wide:
import { GridConfigProvider } from '@workspace/active-grid';
const config = {
columnDefaults: {
size: 150,
enableSorting: true,
},
settings: {
rowHeight: 48,
stripedRows: true,
},
};
function App() {
return (
<GridConfigProvider config={config}>
{/* All tables use these defaults */}
<ActiveGrid {...} />
</GridConfigProvider>
);
}// ✓ Good: Stable row IDs
<ActiveGrid
getRowId={(row) => row.id}
{...}
/>
// ✗ Bad: Using array index (default)
<ActiveGrid {...} />// ✓ Good: Controlled state for persistence
const [sorting, setSorting] = useState<SortingState>([]);
<ActiveGrid
state={{ sorting }}
onSortingChange={setSorting}
{...}
/>
// ✗ Bad: Uncontrolled when you need access
<ActiveGrid {...} />// ✓ Good: gridId provided
<ActiveGrid
gridId="users-grid"
enablePersistence={true}
{...}
/>
// ✗ Bad: Missing gridId
<ActiveGrid
enablePersistence={true}
{...}
/>// ✓ Good: Separate display from data
{
accessorKey: 'price',
header: 'Price',
meta: {
valueFormatter: (value) => `$${value.toFixed(2)}`,
},
}
// ✗ Bad: Formatting in cell
{
accessorKey: 'price',
header: 'Price',
cell: ({ getValue }) => `$${getValue().toFixed(2)}`,
}// ✓ Good: Loading indicator
function UsersTable() {
const { data, isLoading } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
if (isLoading) return <Skeleton />;
return <ActiveGrid data={data} {...} />;
}
// ✗ Bad: No loading state
function UsersTable() {
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return <ActiveGrid data={data || []} {...} />;
}// ✓ Good: Error handling
<ActiveGrid
onDataCommit={async (payload) => {
try {
await api.updateUser(payload.rowId, payload.changes);
toast.success('Updated');
} catch (error) {
toast.error('Update failed');
throw error; // Revert changes
}
}}
{...}
/>
// ✗ Bad: No error handling
<ActiveGrid
onDataCommit={async (payload) => {
await api.updateUser(payload.rowId, payload.changes);
}}
{...}
/>// ✓ Good: Clear labels
{
accessorKey: 'status',
header: 'Status',
cell: ({ getValue }) => (
<Badge aria-label={`Status: ${getValue()}`}>
{getValue()}
</Badge>
),
}
// ✗ Bad: Icon without label
{
cell: () => <CheckCircle />,
}// ✓ Good: Semantic button
{
cell: ({ row }) => (
<button onClick={() => handleEdit(row)}>
Edit
</button>
),
}
// ✗ Bad: div as button
{
cell: ({ row }) => (
<div onClick={() => handleEdit(row)}>
Edit
</div>
),
}/* ✓ Good: WCAG AA compliant */
.grid-row[data-state='selected'] {
background-color: #e7f5ff;
color: #1e3a5f; /* 4.5:1 contrast ratio */
}
/* ✗ Bad: Poor contrast */
.grid-row[data-state='selected'] {
background-color: #e7f5ff;
color: #a0c4ff; /* < 3:1 contrast ratio */
}// ✓ Good: Type-safe
type User = {
id: string;
name: string;
};
<ActiveGrid<User>
columns={columns}
data={users}
onRowClick={(row: User) => console.log(row.name)}
{...}
/>
// ✗ Bad: No type safety
<ActiveGrid
columns={columns}
data={users}
onRowClick={(row: any) => console.log(row.name)}
{...}
/>// ✓ Good: Typed columns
const columns: GridColumnDef<User>[] = [
{ accessorKey: 'name', header: 'Name' },
];
// ✗ Bad: Untyped
const columns = [
{ accessorKey: 'name', header: 'Name' },
];// ✗ Bad: New columns every render
function MyTable() {
const columns = [
{ accessorKey: 'name', header: 'Name' },
];
return <ActiveGrid columns={columns} {...} />;
}
// ✓ Good: Memoized
function MyTable() {
const columns = useMemo(() => [
{ accessorKey: 'name', header: 'Name' },
], []);
return <ActiveGrid columns={columns} {...} />;
}// ✗ Bad: 50,000 rows in client mode
<ActiveGrid
mode="client"
data={fiftyThousandRows}
{...}
/>
// ✓ Good: Use server mode
<ActiveGrid
mode="server"
fetchFn={fetchData}
{...}
/>// ✗ Bad: Selection breaks with server mode
<ActiveGrid
mode="server"
// Missing getRowId
{...}
/>
// ✓ Good: Stable row IDs
<ActiveGrid
mode="server"
getRowId={(row) => row.id}
{...}
/>// ✗ Bad: Mutating original data
onDataCommit={async (payload) => {
payload.oldData.name = payload.newData.name;
}}
// ✓ Good: Create new object
onDataCommit={async (payload) => {
await api.update(payload.rowId, payload.changes);
}}describe('UsersTable', () => {
it('handles empty data', () => {
render(<UsersTable users={[]} />);
expect(screen.getByText('No results')).toBeInTheDocument();
});
it('handles loading state', () => {
render(<UsersTable isLoading={true} />);
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});
it('handles error state', () => {
render(<UsersTable error="Failed to load" />);
expect(screen.getByText('Failed to load')).toBeInTheDocument();
});
});import { useMemo, useCallback } from 'react';
import { useQuery } from '@tanstack/react-query';
import {
ActiveGrid,
GridColumnDef,
getSelectionColumn,
} from '@workspace/active-grid';
type User = {
id: string;
name: string;
email: string;
status: 'active' | 'inactive';
};
function UsersTable() {
// Data fetching with loading/error states
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
// Memoized columns
const columns = useMemo<GridColumnDef<User>[]>(() => [
getSelectionColumn<User>(),
{
accessorKey: 'name',
header: 'Name',
},
{
accessorKey: 'email',
header: 'Email',
},
{
accessorKey: 'status',
header: 'Status',
meta: {
valueFormatter: (value) =>
value === 'active' ? 'Active' : 'Inactive',
},
},
], []);
// Memoized callbacks
const handleRowClick = useCallback((row: User) => {
router.push(`/users/${row.id}`);
}, [router]);
const handleDataCommit = useCallback(async (payload) => {
try {
await api.updateUser(payload.rowId, payload.changes);
toast.success('User updated');
} catch (error) {
toast.error('Update failed');
throw error;
}
}, []);
// Loading state
if (isLoading) {
return <Skeleton className="h-96" />;
}
// Error state
if (error) {
return <Alert variant="destructive">Failed to load users</Alert>;
}
return (
<ActiveGrid<User>
gridId="users-grid"
mode="client"
columns={columns}
data={data || []}
getRowId={(row) => row.id}
enablePersistence={true}
onRowClick={handleRowClick}
onDataCommit={handleDataCommit}
pagination={{
enabled: true,
pageSizeOptions: [10, 25, 50],
}}
/>
);
}