7.3 KiB
7.3 KiB
Streamlined Error & Load State Handling - Quick Reference
🎯 Goal
Standardize error and loading state handling across all GridPilot website pages using user-friendly, accessible, and consistent shared components.
📁 File Structure
apps/website/components/shared/
├── state/
│ ├── LoadingWrapper.tsx # Loading states
│ ├── ErrorDisplay.tsx # Error states
│ ├── EmptyState.tsx # Empty states
│ └── StateContainer.tsx # Combined wrapper
├── hooks/
│ └── useDataFetching.ts # Unified data fetching
└── types/
└── state.types.ts # TypeScript interfaces
🚀 Quick Start Examples
1. Basic Page Implementation
import { useDataFetching } from '@/components/shared/hooks/useDataFetching';
import { LoadingWrapper } from '@/ui/LoadingWrapper';
import { ErrorDisplay } from '@/ui/ErrorDisplay';
import { EmptyState } from '@/ui/EmptyState';
function MyPage() {
const { data, isLoading, error, retry } = useDataFetching({
queryKey: ['myData'],
queryFn: () => myService.getData(),
});
if (isLoading) return <LoadingWrapper variant="full-screen" />;
if (error) return <ErrorDisplay error={error} onRetry={retry} variant="full-screen" />;
if (!data) return <EmptyState icon={Calendar} title="No data" />;
return <MyContent data={data} />;
}
2. Using StateContainer (Recommended)
import { StateContainer } from '@/ui/StateContainer';
function MyPage() {
const { data, isLoading, error, retry } = useDataFetching({
queryKey: ['myData'],
queryFn: () => myService.getData(),
});
return (
<StateContainer
data={data}
isLoading={isLoading}
error={error}
retry={retry}
config={{
loading: { variant: 'skeleton', message: 'Loading...' },
error: { variant: 'full-screen' },
empty: {
icon: Trophy,
title: 'No data found',
description: 'Try refreshing the page',
action: { label: 'Refresh', onClick: retry }
}
}}
>
{(content) => <MyContent data={content} />}
</StateContainer>
);
}
🎨 Component Variants
LoadingWrapper
spinner- Traditional spinner (default)skeleton- Skeleton screensfull-screen- Centered in viewportinline- Compact inlinecard- Card placeholders
ErrorDisplay
full-screen- Full page errorinline- Inline error messagecard- Error cardtoast- Toast notification
EmptyState
default- Standard empty stateminimal- Simple versionfull-page- Full page empty state
🔧 useDataFetching Hook
const {
data, // The fetched data
isLoading, // Initial load
isFetching, // Any fetch (including refetch)
error, // ApiError or null
retry, // Retry failed request
refetch, // Manual refetch
lastUpdated, // Timestamp
isStale // Cache status
} = useDataFetching({
queryKey: ['uniqueKey', id],
queryFn: () => apiService.getData(id),
enabled: true, // Enable/disable query
retryOnMount: true, // Auto-retry on mount
cacheTime: 5 * 60 * 1000, // 5 minutes
staleTime: 1 * 60 * 1000, // 1 minute
maxRetries: 3,
retryDelay: 1000,
onSuccess: (data) => { /* ... */ },
onError: (error) => { /* ... */ },
});
📋 Migration Checklist
Phase 1: Foundation
- Create
components/shared/state/directory - Implement
LoadingWrapper.tsx - Implement
ErrorDisplay.tsx - Implement
EmptyState.tsx - Implement
StateContainer.tsx - Implement
useDataFetching.tshook - Create TypeScript interfaces
- Add comprehensive tests
Phase 2: Core Pages (High Priority)
app/dashboard/page.tsxapp/leagues/[id]/LeagueDetailInteractive.tsxapp/races/[id]/RaceDetailInteractive.tsx
Phase 3: Additional Pages
app/teams/[id]/TeamDetailInteractive.tsxapp/leagues/[id]/schedule/page.tsxapp/races/[id]/results/RaceResultsInteractive.tsxapp/races/[id]/stewarding/RaceStewardingInteractive.tsx- Sponsor pages
- Profile pages
Phase 4: Cleanup
- Remove old loading components
- Remove old error components
- Update documentation
- Team training
🔄 Before & After
Before (Inconsistent)
function DashboardPage() {
const { data, isLoading, error } = useDashboardOverview();
if (isLoading) {
return (
<main className="min-h-screen bg-deep-graphite flex items-center justify-center">
<div className="text-white">Loading dashboard...</div>
</main>
);
}
if (error || !dashboardData) {
return (
<main className="min-h-screen bg-deep-graphite flex items-center justify-center">
<div className="text-red-400">Failed to load dashboard</div>
</main>
);
}
return <DashboardContent data={dashboardData} />;
}
After (Standardized)
function DashboardPage() {
const { data, isLoading, error, retry } = useDataFetching({
queryKey: ['dashboardOverview'],
queryFn: () => dashboardService.getDashboardOverview(),
});
return (
<StateContainer
data={data}
isLoading={isLoading}
error={error}
retry={retry}
config={{
loading: { variant: 'full-screen', message: 'Loading dashboard...' },
error: { variant: 'full-screen' },
empty: {
icon: Activity,
title: 'No dashboard data',
description: 'Try refreshing the page',
action: { label: 'Refresh', onClick: retry }
}
}}
>
{(content) => <DashboardContent data={content} />}
</StateContainer>
);
}
🎯 Key Benefits
- Consistency: Same patterns across all pages
- User-Friendly: Clear messages and helpful actions
- Accessible: ARIA labels, keyboard navigation
- Developer-Friendly: Simple API, less boilerplate
- Maintainable: Single source of truth
- Flexible: Multiple variants for different needs
- Type-Safe: Full TypeScript support
📊 Success Metrics
- ✅ 100% page coverage
- ✅ 60% less error handling code
- ✅ Consistent UX across app
- ✅ Better error recovery
- ✅ Faster development
🔗 Related Files
- Design Document:
STREAMLINED_STATE_HANDLING_DESIGN.md - TypeScript Interfaces:
components/shared/state/types.ts - Component Tests:
components/shared/state/*.test.tsx - Hook Tests:
components/shared/hooks/useDataFetching.test.ts
💡 Tips
- Always use
useDataFetchinginstead of manual state management - Prefer
StateContainerfor complex pages - Use
skeletonvariant for better perceived performance - Enable
retryOnMountfor recoverable errors - Customize config per page needs
- Test all states: loading, error, empty, success
🆘 Troubleshooting
Q: Error not showing?
A: Check if error is instance of ApiError
Q: Loading not visible?
A: Verify isLoading is true and component is rendered
Q: Retry not working?
A: Ensure onRetry prop is passed to ErrorDisplay
Q: Empty state not showing?
A: Check if data is null/undefined and showEmpty is true
For detailed implementation guide, see: STREAMLINED_STATE_HANDLING_DESIGN.md