Files
gridpilot.gg/apps/website/docs/STREAMLINED_STATE_HANDLING_SUMMARY.md
2026-01-06 11:05:16 +01:00

7.4 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 '@/components/shared/state/LoadingWrapper';
import { ErrorDisplay } from '@/components/shared/state/ErrorDisplay';
import { EmptyState } from '@/components/shared/state/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} />;
}
import { StateContainer } from '@/components/shared/state/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

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)

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

  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
  • 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