# Loading & Error States **CRITICAL**: Proper loading and error state handling prevents layout shift and provides better user experience. --- ## ⚠️ CRITICAL RULE: Never Use Early Returns ### The Problem ```typescript // ❌ NEVER DO THIS - Early return with loading spinner const Component = () => { const { data, isLoading } = useQuery(); // WRONG: This causes layout shift and poor UX if (isLoading) { return ; } return ; }; ``` **Why this is bad:** 1. **Layout Shift**: Content position jumps when loading completes 2. **CLS (Cumulative Layout Shift)**: Poor Core Web Vital score 3. **Jarring UX**: Page structure changes suddenly 4. **Lost Scroll Position**: User loses place on page ### The Solutions **Option 1: SuspenseLoader (PREFERRED for new components)** ```typescript import { SuspenseLoader } from '~components/SuspenseLoader'; const HeavyComponent = React.lazy(() => import('./HeavyComponent')); export const MyComponent: React.FC = () => { return ( ); }; ``` **Option 2: LoadingOverlay (for legacy useQuery patterns)** ```typescript import { LoadingOverlay } from '~components/LoadingOverlay'; export const MyComponent: React.FC = () => { const { data, isLoading } = useQuery({ ... }); return ( ); }; ``` --- ## SuspenseLoader Component ### What It Does - Shows loading indicator while lazy components load - Smooth fade-in animation - Prevents layout shift - Consistent loading experience across app ### Import ```typescript import { SuspenseLoader } from '~components/SuspenseLoader'; // Or import { SuspenseLoader } from '@/components/SuspenseLoader'; ``` ### Basic Usage ```typescript ``` ### With useSuspenseQuery ```typescript import { useSuspenseQuery } from '@tanstack/react-query'; import { SuspenseLoader } from '~components/SuspenseLoader'; const Inner: React.FC = () => { // No isLoading needed! const { data } = useSuspenseQuery({ queryKey: ['data'], queryFn: () => api.getData(), }); return ; }; // Outer component wraps in Suspense export const Outer: React.FC = () => { return ( ); }; ``` ### Multiple Suspense Boundaries **Pattern**: Separate loading for independent sections ```typescript export const Dashboard: React.FC = () => { return (
); }; ``` **Benefits:** - Each section loads independently - User sees partial content sooner - Better perceived performance ### Nested Suspense ```typescript export const ParentComponent: React.FC = () => { return ( {/* Parent suspends while loading */} {/* Nested suspense for child */} ); }; ``` --- ## LoadingOverlay Component ### When to Use - Legacy components with `useQuery` (not refactored to Suspense yet) - Overlay loading state needed - Can't use Suspense boundaries ### Usage ```typescript import { LoadingOverlay } from '~components/LoadingOverlay'; export const MyComponent: React.FC = () => { const { data, isLoading } = useQuery({ queryKey: ['data'], queryFn: () => api.getData(), }); return ( {data && } ); }; ``` **What it does:** - Shows semi-transparent overlay with spinner - Content area reserved (no layout shift) - Prevents interaction while loading --- ## Error Handling ### useMuiSnackbar Hook (REQUIRED) **NEVER use react-toastify** - Project standard is MUI Snackbar ```typescript import { useMuiSnackbar } from '@/hooks/useMuiSnackbar'; export const MyComponent: React.FC = () => { const { showSuccess, showError, showInfo, showWarning } = useMuiSnackbar(); const handleAction = async () => { try { await api.doSomething(); showSuccess('Operation completed successfully'); } catch (error) { showError('Operation failed'); } }; return ; }; ``` **Available Methods:** - `showSuccess(message)` - Green success message - `showError(message)` - Red error message - `showWarning(message)` - Orange warning message - `showInfo(message)` - Blue info message ### TanStack Query Error Callbacks ```typescript import { useSuspenseQuery } from '@tanstack/react-query'; import { useMuiSnackbar } from '@/hooks/useMuiSnackbar'; export const MyComponent: React.FC = () => { const { showError } = useMuiSnackbar(); const { data } = useSuspenseQuery({ queryKey: ['data'], queryFn: () => api.getData(), // Handle errors onError: (error) => { showError('Failed to load data'); console.error('Query error:', error); }, }); return ; }; ``` ### Error Boundaries ```typescript import { ErrorBoundary } from 'react-error-boundary'; function ErrorFallback({ error, resetErrorBoundary }) { return ( Something went wrong {error.message} ); } export const MyPage: React.FC = () => { return ( console.error('Boundary caught:', error)} > ); }; ``` --- ## Complete Examples ### Example 1: Modern Component with Suspense ```typescript import React from 'react'; import { Box, Paper } from '@mui/material'; import { useSuspenseQuery } from '@tanstack/react-query'; import { SuspenseLoader } from '~components/SuspenseLoader'; import { myFeatureApi } from '../api/myFeatureApi'; // Inner component uses useSuspenseQuery const InnerComponent: React.FC<{ id: number }> = ({ id }) => { const { data } = useSuspenseQuery({ queryKey: ['entity', id], queryFn: () => myFeatureApi.getEntity(id), }); // data is always defined - no isLoading needed! return (

{data.title}

{data.description}

); }; // Outer component provides Suspense boundary export const OuterComponent: React.FC<{ id: number }> = ({ id }) => { return ( ); }; export default OuterComponent; ``` ### Example 2: Legacy Pattern with LoadingOverlay ```typescript import React from 'react'; import { Box } from '@mui/material'; import { useQuery } from '@tanstack/react-query'; import { LoadingOverlay } from '~components/LoadingOverlay'; import { myFeatureApi } from '../api/myFeatureApi'; export const LegacyComponent: React.FC<{ id: number }> = ({ id }) => { const { data, isLoading, error } = useQuery({ queryKey: ['entity', id], queryFn: () => myFeatureApi.getEntity(id), }); return ( {error && } {data && } ); }; ``` ### Example 3: Error Handling with Snackbar ```typescript import React from 'react'; import { useSuspenseQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Button } from '@mui/material'; import { useMuiSnackbar } from '@/hooks/useMuiSnackbar'; import { myFeatureApi } from '../api/myFeatureApi'; export const EntityEditor: React.FC<{ id: number }> = ({ id }) => { const queryClient = useQueryClient(); const { showSuccess, showError } = useMuiSnackbar(); const { data } = useSuspenseQuery({ queryKey: ['entity', id], queryFn: () => myFeatureApi.getEntity(id), onError: () => { showError('Failed to load entity'); }, }); const updateMutation = useMutation({ mutationFn: (updates) => myFeatureApi.update(id, updates), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['entity', id] }); showSuccess('Entity updated successfully'); }, onError: () => { showError('Failed to update entity'); }, }); return ( ); }; ``` --- ## Loading State Anti-Patterns ### ❌ What NOT to Do ```typescript // ❌ NEVER - Early return if (isLoading) { return ; } // ❌ NEVER - Conditional rendering {isLoading ? : } // ❌ NEVER - Layout changes if (isLoading) { return ( ); } return ( // Different height! ); ``` ### ✅ What TO Do ```typescript // ✅ BEST - useSuspenseQuery + SuspenseLoader // ✅ ACCEPTABLE - LoadingOverlay // ✅ OK - Inline skeleton with same layout {isLoading ? : } ``` --- ## Skeleton Loading (Alternative) ### MUI Skeleton Component ```typescript import { Skeleton, Box } from '@mui/material'; export const MyComponent: React.FC = () => { const { data, isLoading } = useQuery({ ... }); return ( {isLoading ? ( <> ) : ( <> {data.title} {data.description} )} ); }; ``` **Key**: Skeleton must have **same layout** as actual content (no shift) --- ## Summary **Loading States:** - ✅ **PREFERRED**: SuspenseLoader + useSuspenseQuery (modern pattern) - ✅ **ACCEPTABLE**: LoadingOverlay (legacy pattern) - ✅ **OK**: Skeleton with same layout - ❌ **NEVER**: Early returns or conditional layout **Error Handling:** - ✅ **ALWAYS**: useMuiSnackbar for user feedback - ❌ **NEVER**: react-toastify - ✅ Use onError callbacks in queries/mutations - ✅ Error boundaries for component-level errors **See Also:** - [component-patterns.md](component-patterns.md) - Suspense integration - [data-fetching.md](data-fetching.md) - useSuspenseQuery details