# 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