Skip to main content

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-grid imports in src/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​

PropTypeRequiredDefaultDescription
rowsGridRowsProp<T>Yes-Array of data rows. Each row must have a unique id property
columnsTokenAwareGridColDef[]Yes-Column configuration defining headers, widths, and rendering. Supports spacing token names for width properties
loadingbooleanNofalseShows loading overlay when true
searchConfigSearchConfig<T>No-Configuration for search functionality
sortConfigSortConfigNo-Configuration for sorting options
paginationConfigPaginationConfigNo-Configuration for pagination
onRowClick(row: T) => voidNo-Callback fired when a row is clicked
actionColumn(row: T) => ReactNodeNo-Render function for custom actions column
emptyStatePropsInterfaceEmptyStatePropsNo-Full customization of empty state with icon, description, and actions. Takes precedence over emptyStateMessage
emptyStateMessagestringNo"No results found"Message shown when no rows are available
errorstring | ReactNodeNo-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 slots API 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:

  1. First, consider if the feature can be added to DataGridWrapper
  2. If not, discuss with the team
  3. 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:

  1. Opening an issue to discuss adding it to DataGridWrapper
  2. 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.

  • 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

See Also​