Admin App Modular Component Patterns
This document outlines the modular, reusable component patterns established in the admin application to reduce code duplication and improve maintainability.
Core Modular Components
1. NotificationProvider & useNotification
Purpose: Centralized notification system replacing manual Snackbar implementations.
Usage:
import { useNotification } from '@/hooks/useNotification'
function MyComponent() {
const { success, error, warning, info } = useNotification()
const handleAction = async () => {
try {
await someApiCall()
success('Action completed successfully')
} catch (err) {
error('Action failed: ' + err.message)
}
}
}
Benefits:
- Single global notification system
- Consistent styling and positioning
- No need to manage Snackbar state manually
- Clean, semantic API
Migration Pattern:
// ❌ BEFORE - Manual Snackbar
const [snackbar, setSnackbar] = useState({ open: false, message: '', severity: 'success' })
const handleAction = () => {
setSnackbar({ open: true, message: 'Success!', severity: 'success' })
}
<Snackbar open={snackbar.open} onClose={handleClose}>
<Alert severity={snackbar.severity}>{snackbar.message}</Alert>
</Snackbar>
// ✅ AFTER - useNotification
const { success } = useNotification()
const handleAction = () => {
success('Success!')
}
// No Snackbar component needed!
2. ConfirmationProvider & useConfirmation
Purpose: Centralized confirmation dialog system replacing manual Dialog implementations.
Usage:
import { useConfirm } from '@/hooks/useConfirmation'
function MyComponent() {
const confirm = useConfirm()
const handleDelete = async (item: Item) => {
const confirmed = await confirm({
title: 'Delete Item',
message: `Are you sure you want to delete "${item.name}"? This action cannot be undone.`,
type: 'error',
confirmText: 'Delete',
cancelText: 'Cancel',
confirmColor: 'error'
})
if (confirmed) {
// Proceed with deletion
await deleteItem(item.id)
}
}
}
Dialog Types:
warning: Orange icon, for potentially dangerous actionserror: Red icon, for destructive actionsinfo: Blue icon, for informational confirmationsquestion: Default blue, for general confirmations
Benefits:
- Promise-based API (no callback hell)
- Consistent dialog styling
- No need to manage dialog state
- Keyboard support (Enter to confirm)
Migration Pattern:
// ❌ BEFORE - Manual Dialog
const [dialogOpen, setDialogOpen] = useState(false)
const [selectedItem, setSelectedItem] = useState(null)
const handleClick = (item) => {
setSelectedItem(item)
setDialogOpen(true)
}
const handleConfirm = () => {
deleteItem(selectedItem.id)
setDialogOpen(false)
}
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)}>
<DialogTitle>Confirm</DialogTitle>
<DialogContent>Are you sure?</DialogContent>
<DialogActions>
<Button onClick={() => setDialogOpen(false)}>Cancel</Button>
<Button onClick={handleConfirm}>Confirm</Button>
</DialogActions>
</Dialog>
// ✅ AFTER - useConfirmation
const confirm = useConfirm()
const handleClick = async (item) => {
const confirmed = await confirm({
title: 'Confirm',
message: 'Are you sure?',
})
if (confirmed) {
deleteItem(item.id)
}
}
// No Dialog component needed!
3. DataTable Component
Purpose: Standardized data table with sorting, pagination, actions, and expansion.
Features:
- Built-in sorting with HeaderSortCell
- Pagination controls
- Row actions (menu or buttons)
- Expandable rows
- Loading/error/empty states via TableStateWrapper
- Status badges, date formatting
- Sticky headers
Usage:
import DataTable, { ColumnConfig } from '@/components/common/DataTable'
const columns: ColumnConfig<Job>[] = [
{
id: 'name',
label: 'Job Name',
sortable: true,
accessor: 'name',
width: 300
},
{
id: 'status',
label: 'Status',
format: 'status',
statusType: (job) => job.enabled ? 'enabled' : 'disabled',
accessor: (job) => job.enabled ? 'Enabled' : 'Disabled'
},
{
id: 'lastRun',
label: 'Last Run',
format: 'date',
accessor: 'last_executed_at'
},
{
id: 'success_rate',
label: 'Success Rate',
format: 'number',
accessor: (job) => `${job.success_rate.toFixed(1)}%`,
sortable: true
}
]
const actions: TableAction<Job>[] = [
{
label: 'Edit',
onClick: (job) => handleEdit(job),
icon: <EditIcon />
},
{
label: 'Delete',
onClick: (job) => handleDelete(job),
icon: <DeleteIcon />,
color: 'error'
}
]
<DataTable
data={jobs}
columns={columns}
rowKey="id"
tableState={{ isLoading, error, isEmpty: jobs.length === 0 }}
paginationState={pagination}
onPageChange={handlePageChange}
sortState={{ field: 'name', direction: 'asc' }}
onSort={handleSort}
actions={actions}
expandableRow={{
render: (job) => <JobDetails job={job} />
}}
/>
Column Formats:
text: Plain text (default)number: Formatted number with localedate: Formatted date/timestatus: Status chip with colorscode: Monospace font for codecustom: Userenderprop for full control
Benefits:
- Consistent table styling across the app
- Reduces boilerplate by ~200-300 lines per table
- Built-in accessibility features
- Type-safe column configuration
Mutation Notification Pattern
When working with React Query mutations, combine useNotification with mutation callbacks:
const { success, error } = useNotification()
const queryClient = useQueryClient()
const updateMutation = useMutation(
(data) => apiService.update(data),
{
onSuccess: () => {
success('Updated successfully')
queryClient.invalidateQueries(['items'])
},
onError: (err: any) => {
error(err.response?.data?.message || 'Update failed')
}
}
)
Advanced Pattern with useConfirmation:
const { success, error } = useNotification()
const confirm = useConfirm()
const queryClient = useQueryClient()
const deleteMutation = useMutation(
(id: number) => apiService.delete(id),
{
onSuccess: () => {
success('Deleted successfully')
queryClient.invalidateQueries(['items'])
},
onError: (err: any) => {
error(err.response?.data?.message || 'Delete failed')
}
}
)
const handleDelete = async (item: Item) => {
const confirmed = await confirm({
title: 'Delete Item',
message: `Delete "${item.name}"?`,
type: 'error',
confirmText: 'Delete'
})
if (confirmed) {
deleteMutation.mutate(item.id)
}
}
Best Practices
1. Always Use Modular Components
DO:
- Use
useNotificationfor all user feedback - Use
useConfirmationfor all confirmations - Use
DataTablefor tabular data when possible - Use existing components from
@/components/common
DON'T:
- Create manual Snackbar implementations
- Create manual Dialog implementations
- Build custom tables from scratch without checking DataTable first
- Duplicate component logic
2. Consistent Error Handling
// ✅ Good - Consistent error message extraction
onError: (error: any) => {
notifyError(error.response?.data?.message || 'Operation failed')
}
// ❌ Bad - Inconsistent error handling
onError: (error) => {
setSnackbar({ open: true, message: error.message, severity: 'error' })
}
3. Type Safety
// ✅ Good - Type-safe column configuration
const columns: ColumnConfig<Job>[] = [...]
// ✅ Good - Type-safe actions
const actions: TableAction<Job>[] = [...]
4. Component Composition
Build complex UIs by composing modular components:
function JobsPage() {
const { success, error } = useNotification()
const confirm = useConfirm()
const { data: jobs, isLoading, error: loadError } = useJobs()
const handleDelete = async (job: Job) => {
const confirmed = await confirm({
title: 'Delete Job',
message: `Delete "${job.name}"?`,
type: 'error'
})
if (confirmed) {
try {
await deleteJob(job.id)
success('Job deleted')
} catch (err) {
error('Failed to delete job')
}
}
}
return (
<Page title="Jobs">
<DataTable
data={jobs || []}
columns={jobColumns}
rowKey="id"
tableState={{ isLoading, error: loadError }}
actions={[
{ label: 'Delete', onClick: handleDelete }
]}
/>
</Page>
)
}
Migration Checklist
When refactoring a component to use modular patterns:
- Replace manual Snackbar with
useNotification - Replace manual Dialog with
useConfirmation - Consider using
DataTablefor tables - Remove unnecessary state management
- Remove manual JSX for notifications/dialogs
- Update mutation callbacks to use notification hooks
- Add proper TypeScript types
- Test all user interactions
Examples
✅ Good Examples (Already Refactored)
JobDetailsView.tsx- Uses useNotificationJobsTable.tsx- Uses useConfirmationApiRecords.tsx- Uses both useNotification and useConfirmationPeople.tsx- Uses DataTableLegalStatuses.tsx- Uses DataTable
🔄 Needs Refactoring
Check other components for:
- Manual Snackbar implementations
- Manual Dialog implementations
- Custom table implementations that could use DataTable
Future Improvements
- Mutation Notification Wrapper: Create a HOC or utility to automatically wrap mutations with notifications
- DataTable Presets: Create preset configurations for common table patterns
- Form Components: Standardize form components (inputs, selects, etc.)
- Loading States: Standardize loading indicators
- Error Boundaries: Add error boundaries for better error handling
Additional Resources
- Components:
admin/src/components/common/ - Hooks:
admin/src/hooks/ - Types:
admin/src/types/ - Examples: Look at recently refactored components
Last Updated: 2025-11-13 Maintained By: Development Team