264 lines
7.3 KiB
Markdown
264 lines
7.3 KiB
Markdown
# 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
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```typescript
|
|
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 screens
|
|
- `full-screen` - Centered in viewport
|
|
- `inline` - Compact inline
|
|
- `card` - Card placeholders
|
|
|
|
### ErrorDisplay
|
|
- `full-screen` - Full page error
|
|
- `inline` - Inline error message
|
|
- `card` - Error card
|
|
- `toast` - Toast notification
|
|
|
|
### EmptyState
|
|
- `default` - Standard empty state
|
|
- `minimal` - Simple version
|
|
- `full-page` - Full page empty state
|
|
|
|
## 🔧 useDataFetching Hook
|
|
|
|
```typescript
|
|
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.ts` hook
|
|
- [ ] Create TypeScript interfaces
|
|
- [ ] Add comprehensive tests
|
|
|
|
### Phase 2: Core Pages (High Priority)
|
|
- [ ] `app/dashboard/page.tsx`
|
|
- [ ] `app/leagues/[id]/LeagueDetailInteractive.tsx`
|
|
- [ ] `app/races/[id]/RaceDetailInteractive.tsx`
|
|
|
|
### Phase 3: Additional Pages
|
|
- [ ] `app/teams/[id]/TeamDetailInteractive.tsx`
|
|
- [ ] `app/leagues/[id]/schedule/page.tsx`
|
|
- [ ] `app/races/[id]/results/RaceResultsInteractive.tsx`
|
|
- [ ] `app/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)
|
|
```typescript
|
|
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)
|
|
```typescript
|
|
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
|
|
|
|
1. **Consistency**: Same patterns across all pages
|
|
2. **User-Friendly**: Clear messages and helpful actions
|
|
3. **Accessible**: ARIA labels, keyboard navigation
|
|
4. **Developer-Friendly**: Simple API, less boilerplate
|
|
5. **Maintainable**: Single source of truth
|
|
6. **Flexible**: Multiple variants for different needs
|
|
7. **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
|
|
|
|
1. **Always use `useDataFetching`** instead of manual state management
|
|
2. **Prefer `StateContainer`** for complex pages
|
|
3. **Use `skeleton` variant** for better perceived performance
|
|
4. **Enable `retryOnMount`** for recoverable errors
|
|
5. **Customize config** per page needs
|
|
6. **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` |