Skip to main content

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 actions
  • error: Red icon, for destructive actions
  • info: Blue icon, for informational confirmations
  • question: 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 locale
  • date: Formatted date/time
  • status: Status chip with colors
  • code: Monospace font for code
  • custom: Use render prop 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 useNotification for all user feedback
  • Use useConfirmation for all confirmations
  • Use DataTable for 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 DataTable for 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 useNotification
  • JobsTable.tsx - Uses useConfirmation
  • ApiRecords.tsx - Uses both useNotification and useConfirmation
  • People.tsx - Uses DataTable
  • LegalStatuses.tsx - Uses DataTable

🔄 Needs Refactoring

Check other components for:

  • Manual Snackbar implementations
  • Manual Dialog implementations
  • Custom table implementations that could use DataTable

Future Improvements

  1. Mutation Notification Wrapper: Create a HOC or utility to automatically wrap mutations with notifications
  2. DataTable Presets: Create preset configurations for common table patterns
  3. Form Components: Standardize form components (inputs, selects, etc.)
  4. Loading States: Standardize loading indicators
  5. 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