DataGridWrapper
Overview
The DataGridWrapper is a standardized, reusable component that wraps Material-UI's DataGrid to provide consistent table functionality across the Talawa Admin application. It includes built-in support for search, sorting, pagination, loading states, and error handling.
Key Features:
- Integrated search with configurable fields
- Flexible sorting with custom options
- Built-in pagination controls
- Custom loading states and error handling
- Action column support
- Fully type-safe with TypeScript generics
- i18n ready
Why Use DataGridWrapper:
- Consistency: Ensures all data grids across the application have uniform behavior and appearance
- Policy Enforcement: The linter prevents direct
@mui/x-data-gridimports insrc/screens/**, enforcing use of this wrapper - Reduced Boilerplate: Common features like search and pagination are pre-integrated
- Maintainability: Changes to grid behavior can be made in one place
Component Location
src/shared-components/DataGridWrapper/
├── DataGridWrapper.tsx
├── DataGridWrapper.spec.tsx
├── DataGridWrapper.module.css
├── DataGridLoadingOverlay.tsx
└── DataGridErrorOverlay.tsx
Type Definitions:
src/types/DataGridWrapper/interface.ts
Quick Start
Basic Usage
import { DataGridWrapper } from 'src/shared-components/DataGridWrapper/DataGridWrapper';
import type { TokenAwareGridColDef } from 'src/types/DataGridWrapper/interface';
type User = { id: string; name: string; email: string };
const columns: TokenAwareGridColDef[] = [
{ field: 'name', headerName: 'Name', minWidth: 'space-17' }, // 220px
{ field: 'email', headerName: 'Email', minWidth: 'space-18' }, // 250px
];
const users: User[] = [
{ id: '1', name: 'John Doe', email: 'john@example.com' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com' },
];
<DataGridWrapper<User> rows={users} columns={columns} />;
Component API
Props Reference
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
rows | GridRowsProp<T> | Yes | - | Array of data rows. Each row must have a unique id property |
columns | TokenAwareGridColDef[] | Yes | - | Column configuration defining headers, widths, and rendering. Supports spacing token names for width properties |
loading | boolean | No | false | Shows loading overlay when true |
searchConfig | SearchConfig<T> | No | - | Configuration for search functionality |
sortConfig | SortConfig | No | - | Configuration for sorting options |
paginationConfig | PaginationConfig | No | - | Configuration for pagination |
onRowClick | (row: T) => void | No | - | Callback fired when a row is clicked |
actionColumn | (row: T) => ReactNode | No | - | Render function for custom actions column |
emptyStateProps | InterfaceEmptyStateProps | No | - | Full customization of empty state with icon, description, and actions. Takes precedence over emptyStateMessage |
emptyStateMessage | string | No | "No results found" | Message shown when no rows are available |
error | string | ReactNode | No | - | Error message or component to display |
SearchConfig
interface SearchConfig<T> {
/** Enable the search bar */
enabled: boolean;
/** Fields to search across */
fields: Array<keyof T & string>;
/** Custom placeholder text */
placeholder?: string;
/** Debounce delay in milliseconds */
debounceMs?: number;
}
SortConfig
interface SortConfig {
/** Default field to sort by */
defaultSortField?: string;
/** Default sort direction */
defaultSortOrder?: 'asc' | 'desc';
/** Array of sorting options */
sortingOptions?: Array<{
label: string;
value: string | number;
}>;
}
PaginationConfig
interface PaginationConfig {
/** Enable pagination */
enabled: boolean;
/** Default number of rows per page */
defaultPageSize?: number;
/** Available page size options */
pageSizeOptions?: number[];
}
Usage Examples
With Custom Empty State
<DataGridWrapper<User>
rows={users}
columns={columns}
emptyStateProps={{
icon: 'users',
message: 'noUsersFound',
description: 'inviteFirstUser',
action: {
label: 'inviteUser',
onClick: handleInvite,
variant: 'primary',
},
dataTestId: 'users-empty-state',
}}
/>
WithSearch
<DataGridWrapper<User>
rows={users}
columns={columns}
searchConfig={{
enabled: true,
fields: ['name', 'email'],
placeholder: 'Search users...',
}}
/>
The search performs case-insensitive filtering across all specified fields.
With Sorting
<DataGridWrapper<User>
rows={users}
columns={columns}
sortConfig={{
defaultSortField: 'name',
defaultSortOrder: 'asc',
sortingOptions: [
{ label: 'Name (A-Z)', value: 'name_asc' },
{ label: 'Name (Z-A)', value: 'name_desc' },
{ label: 'Email (A-Z)', value: 'email_asc' },
{ label: 'Email (Z-A)', value: 'email_desc' },
],
}}
/>
With Pagination
<DataGridWrapper<User>
rows={users}
columns={columns}
paginationConfig={{
enabled: true,
defaultPageSize: 25,
pageSizeOptions: [10, 25, 50, 100],
}}
/>
With Loading State
const { data, loading } = useQuery(GET_USERS);
<DataGridWrapper<User>
rows={data?.users || []}
columns={columns}
loading={loading}
/>;
The component displays a custom loading overlay using the LoadingState component.
With Error Handling
const { data, loading, error } = useQuery(GET_USERS);
<DataGridWrapper<User>
rows={data?.users || []}
columns={columns}
loading={loading}
error={error ? 'Failed to load users. Please try again.' : undefined}
/>;
The error state is displayed using a custom error overlay component (DataGridErrorOverlay) that appears in place of the data grid, providing a consistent UX with the loading and empty states. The overlay includes an error icon and message, with proper accessibility attributes (role="alert", aria-live="assertive").
[!NOTE] The error overlay uses the DataGrid's
slotsAPI for consistency. When an error is present, it takes precedence over the empty state overlay.
With Action Column
import { IconButton } from '@mui/material';
import { Edit, Delete } from '@mui/icons-material';
<DataGridWrapper<User>
rows={users}
columns={columns}
actionColumn={(row) => (
<>
<IconButton onClick={() => handleEdit(row.id)} aria-label="Edit user">
<Edit />
</IconButton>
<IconButton onClick={() => handleDelete(row.id)} aria-label="Delete user">
<Delete />
</IconButton>
</>
)}
/>;
With Row Click Handler
<DataGridWrapper<User>
rows={users}
columns={columns}
onRowClick={(row) => {
navigate(`/admin/users/${row.id}`);
}}
/>
Complete Example
import React from 'react';
import { useQuery } from '@apollo/client';
import { useNavigate } from 'react-router-dom';
import { DataGridWrapper } from 'src/shared-components/DataGridWrapper/DataGridWrapper';
import { GET_USERS } from 'src/GraphQl/Queries/Queries';
import type { TokenAwareGridColDef } from 'src/types/DataGridWrapper/interface';
type User = {
id: string;
name: string;
email: string;
role: string;
};
export const UsersScreen = () => {
const navigate = useNavigate();
const { data, loading, error } = useQuery(GET_USERS);
const columns: TokenAwareGridColDef[] = [
{ field: 'name', headerName: 'Name', minWidth: 'space-17' }, // 220px
{ field: 'email', headerName: 'Email', minWidth: 'space-18' }, // 250px
{ field: 'role', headerName: 'Role', minWidth: 'space-15' }, // 150px
];
return (
<DataGridWrapper<User>
rows={data?.users || []}
columns={columns}
loading={loading}
error={error ? 'Failed to load users' : undefined}
searchConfig={{
enabled: true,
fields: ['name', 'email', 'role'],
placeholder: 'Search users by name, email, or role...',
}}
sortConfig={{
defaultSortField: 'name',
defaultSortOrder: 'asc',
sortingOptions: [
{ label: 'Name (A-Z)', value: 'name_asc' },
{ label: 'Name (Z-A)', value: 'name_desc' },
{ label: 'Email (A-Z)', value: 'email_asc' },
{ label: 'Email (Z-A)', value: 'email_desc' },
],
}}
paginationConfig={{
enabled: true,
defaultPageSize: 25,
pageSizeOptions: [10, 25, 50, 100],
}}
onRowClick={(row) => navigate(`/admin/users/${row.id}`)}
emptyStateMessage="No users found"
/>
);
};
Migration Guide
From Direct DataGrid Usage
If you're currently using @mui/x-data-grid directly in src/screens/**, follow these steps to migrate:
Step 1: Replace Import
Before:
import { DataGrid } from '@mui/x-data-grid';
import type { GridColDef } from '@mui/x-data-grid';
After:
import { DataGridWrapper } from 'src/shared-components/DataGridWrapper/DataGridWrapper';
import type { TokenAwareGridColDef } from 'src/types/DataGridWrapper/interface';
Step 2: Update Component Usage
Before:
<DataGrid
rows={users}
columns={columns}
loading={loading}
pageSize={25}
rowsPerPageOptions={[10, 25, 50]}
onRowClick={(params) => handleRowClick(params.row)}
/>
After:
<DataGridWrapper<User>
rows={users}
columns={columns}
loading={loading}
paginationConfig={{
enabled: true,
defaultPageSize: 25,
pageSizeOptions: [10, 25, 50],
}}
onRowClick={(row) => handleRowClick(row)}
/>
Step 3: Move Search Logic
If you have custom search logic:
Before:
const [searchTerm, setSearchTerm] = useState('');
const filteredUsers = users.filter(
(u) =>
u.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
u.email.toLowerCase().includes(searchTerm.toLowerCase()),
);
<>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
<DataGrid rows={filteredUsers} columns={columns} />
</>;
After:
<DataGridWrapper<User>
rows={users}
columns={columns}
searchConfig={{
enabled: true,
fields: ['name', 'email'],
placeholder: 'Search...',
}}
/>
Step 4: Move Sorting Logic
If you have custom sorting:
Before:
const [sortModel, setSortModel] = useState([{ field: 'name', sort: 'asc' }]);
<DataGrid
rows={users}
columns={columns}
sortModel={sortModel}
onSortModelChange={setSortModel}
/>;
After:
<DataGridWrapper<User>
rows={users}
columns={columns}
sortConfig={{
defaultSortField: 'name',
defaultSortOrder: 'asc',
sortingOptions: [
{ label: 'Name (A-Z)', value: 'name_asc' },
{ label: 'Name (Z-A)', value: 'name_desc' },
],
}}
/>
Common Migration Patterns
Pattern 1: Empty State Handling
Before:
{
users.length === 0 && !loading ? (
<div>No users found</div>
) : (
<DataGrid rows={users} columns={columns} />
);
}
After:
<DataGridWrapper<User>
rows={users}
columns={columns}
emptyStateMessage="No users found"
/>
Pattern 2: Error Handling
Before:
{
error ? (
<div>Error: {error.message}</div>
) : (
<DataGrid rows={users} columns={columns} />
);
}
After:
<DataGridWrapper<User>
rows={users}
columns={columns}
error={error ? error.message : undefined}
/>
The error now displays as an overlay within the DataGrid using the slots API, providing a consistent experience with the loading and empty states.
Pattern 3: Action Buttons
Before:
const columns: GridColDef[] = [
{ field: 'name', headerName: 'Name' },
{
field: 'actions',
headerName: 'Actions',
renderCell: (params) => (
<button onClick={() => handleEdit(params.row)}>Edit</button>
),
},
];
After:
const columns: GridColDef[] = [{ field: 'name', headerName: 'Name' }];
<DataGridWrapper<User>
rows={users}
columns={columns}
actionColumn={(row) => <button onClick={() => handleEdit(row)}>Edit</button>}
/>;
Common Patterns and Best Practices
Type Safety
Always provide the generic type parameter for full type safety:
// ✅ Good: Type-safe
<DataGridWrapper<User>
rows={users}
columns={columns}
onRowClick={(row) => {
// `row` is correctly typed as User
console.log(row.name);
}}
/>
// ❌ Bad: No type safety
<DataGridWrapper
rows={users}
columns={columns}
/>
Column Configuration
Define columns outside the component to prevent re-renders:
import type { TokenAwareGridColDef } from 'src/types/DataGridWrapper/interface';
// ✅ Good: Defined outside component
const columns: TokenAwareGridColDef[] = [
{ field: 'name', headerName: 'Name', minWidth: 'space-17' },
{ field: 'email', headerName: 'Email', minWidth: 'space-18' },
];
export const UsersScreen = () => {
return <DataGridWrapper<User> rows={users} columns={columns} />;
};
// ❌ Bad: Defined inside component (re-creates on every render)
export const UsersScreen = () => {
const columns = [{ field: 'name', headerName: 'Name', minWidth: 'space-17' }];
return <DataGridWrapper<User> rows={users} columns={columns} />;
};
Column Width Tokens
MUI DataGrid requires numeric pixel values for width, minWidth, and maxWidth. CSS var() strings do not work and will silently break column sizing at runtime.
Use TokenAwareGridColDef from src/types/DataGridWrapper/interface and spacing token names instead. The DataGridWrapper automatically converts token names to pixel values via convertTokenColumns().
import type { TokenAwareGridColDef } from 'src/types/DataGridWrapper/interface';
// ✅ Correct: spacing token names
const columns: TokenAwareGridColDef[] = [
{ field: 'name', headerName: 'Name', minWidth: 'space-15' }, // converted to 150px
{ field: 'email', headerName: 'Email', width: 'space-18' }, // converted to 250px
{ field: 'role', headerName: 'Role', minWidth: 'space-15' }, // converted to 150px
];
// ✅ Also correct: raw numeric values still work
const columns: TokenAwareGridColDef[] = [
{ field: 'name', headerName: 'Name', minWidth: 150 },
];
// ❌ Wrong: var() strings break DataGrid at runtime
const columns = [
{ field: 'name', minWidth: 'var(--vw-80)' }, // silently broken
];
// ❌ Wrong: hardcoded numbers with GridColDef (fails the design token validator)
const columns: GridColDef[] = [
{ field: 'name', minWidth: 150 }, // flagged by validator
];
See src/utils/tokenValues.ts for the full token-to-pixel mapping (e.g., 'space-15' = 150px, 'space-17' = 220px, 'space-18' = 250px).
Search Fields
Only include searchable text fields in searchConfig.fields:
// ✅ Good: Only text fields
searchConfig={{
enabled: true,
fields: ['name', 'email', 'organization'],
}}
// ❌ Bad: Including non-text fields
searchConfig={{
enabled: true,
fields: ['name', 'createdAt', 'isActive'], // createdAt and isActive won't search well
}}
Pagination
Enable pagination for large datasets:
// ✅ Good: Pagination enabled for large lists
<DataGridWrapper<User>
rows={users} // 1000+ users
columns={columns}
paginationConfig={{
enabled: true,
defaultPageSize: 25,
}}
/>
// ❌ Bad: No pagination for large dataset
<DataGridWrapper<User>
rows={users} // 1000+ users - will cause performance issues
columns={columns}
/>
Loading and Error States
Always handle loading and error states:
// ✅ Good: Handles all states
const { data, loading, error } = useQuery(GET_USERS);
<DataGridWrapper<User>
rows={data?.users || []}
columns={columns}
loading={loading}
error={error ? 'Failed to load users' : undefined}
emptyStateMessage="No users found"
/>;
i18n Support
Use translation keys for user-facing text:
import { useTranslation } from 'react-i18next';
const { t } = useTranslation('users');
<DataGridWrapper<User>
rows={users}
columns={columns}
searchConfig={{
enabled: true,
fields: ['name', 'email'],
placeholder: t('searchPlaceholder'),
}}
emptyStateMessage={t('noUsersFound')}
error={error ? t('loadError') : undefined}
/>;
Accessibility
Ensure action buttons have proper aria labels:
<DataGridWrapper<User>
rows={users}
columns={columns}
actionColumn={(row) => (
<>
<IconButton
onClick={() => handleEdit(row.id)}
aria-label={`Edit user ${row.name}`}
>
<Edit />
</IconButton>
<IconButton
onClick={() => handleDelete(row.id)}
aria-label={`Delete user ${row.name}`}
>
<Delete />
</IconButton>
</>
)}
/>
Sort Format Validation
The DataGridWrapper validates sort formats and provides helpful console warnings for debugging:
// ✅ Good: Correct sort format
sortConfig={{
sortingOptions: [
{ label: 'Name (A-Z)', value: 'name_asc' }, // Correct
{ label: 'Name (Z-A)', value: 'name_desc' }, // Correct
],
}}
// ❌ Bad: Invalid sort format - will log warning
sortConfig={{
sortingOptions: [
{ label: 'Name', value: 'name' }, // Missing sort direction
{ label: 'Email', value: 'email-ascending' }, // Wrong separator
],
}}
If an invalid format is detected, you'll see a console warning:
[DataGridWrapper] Invalid sort format: "name". Expected format: "field_asc" or "field_desc"
This helps developers quickly identify and fix configuration errors during development.
Linter Enforcement
Direct usage of @mui/x-data-grid and @mui/x-data-grid-pro is enforced via ESLint
(no-restricted-imports) in eslint.config.js, with wrapper exemptions managed in
scripts/eslint/config/exemptions.ts.
Only the DataGridWrapper and its associated type definitions are allowed to import
these packages directly. All other usage must go through the standardized wrapper.
Linter runs:
- Pre-commit (via lint-staged)
- Pull request CI (via GitHub Actions)
If you need to use DataGrid features not supported by DataGridWrapper:
- First, consider if the feature can be added to
DataGridWrapper - If not, discuss with the team
- The component may need to be refactored or enhanced
Testing
When testing components that use DataGridWrapper:
import { render, screen } from '@testing-library/react';
import { DataGridWrapper } from 'src/shared-components/DataGridWrapper/DataGridWrapper';
test('renders user data correctly', () => {
const users = [{ id: '1', name: 'John Doe', email: 'john@example.com' }];
const columns = [
{ field: 'name', headerName: 'Name' },
{ field: 'email', headerName: 'Email' },
];
render(<DataGridWrapper<User> rows={users} columns={columns} />);
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
test('renders empty state message', () => {
render(
<DataGridWrapper<User>
rows={[]}
columns={[]}
emptyStateMessage="No data available"
/>,
);
expect(screen.getByText('No data available')).toBeInTheDocument();
});
FAQ
Q: Can I use MUI DataGrid props not exposed by DataGridWrapper?
A: DataGridWrapper exposes the most commonly used props. If you need additional DataGrid functionality, consider:
- Opening an issue to discuss adding it to DataGridWrapper
- Proposing changes to enhance the wrapper component
Q: How do I customize column rendering?
A: Use the standard MUI renderCell property on TokenAwareGridColDef:
const columns: TokenAwareGridColDef[] = [
{
field: 'status',
headerName: 'Status',
renderCell: (params) => (
<Chip
label={params.value}
color={params.value === 'active' ? 'success' : 'default'}
/>
),
},
];
Q: How do I handle server-side pagination/sorting?
A: Currently, DataGridWrapper supports client-side pagination and sorting. For server-side features, you'll need to implement the logic before passing data to the component:
const { data, loading } = useQuery(GET_USERS, {
variables: {
page: currentPage,
pageSize: 25,
sortBy: sortField,
sortOrder: sortOrder,
},
});
<DataGridWrapper<User>
rows={data?.users || []}
columns={columns}
loading={loading}
/>;
Q: Can I disable pagination?
A: Yes, simply don't provide a paginationConfig or set paginationConfig.enabled to false.
Q: How do I style the DataGrid?
A: The DataGrid uses MUI's styling system. You can use the sx prop on columns or wrap DataGridWrapper in a styled container. For global styling changes, modify DataGridWrapper.module.css.
Related Components
- SearchBar: Used internally by DataGridWrapper for search functionality
- SortingButton: Used internally for sorting dropdown
- DataGridLoadingOverlay: Custom loading overlay component displayed via DataGrid slots
- DataGridErrorOverlay: Custom error overlay component displayed via DataGrid slots when errors occur
- EmptyState: Displayed via DataGrid slots when no data is available