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...',
}}
/>