veza/apps/web/src/docs/LOADING_STATES_PATTERN.md

152 lines
3.2 KiB
Markdown
Raw Normal View History

# Loading States Pattern Guide
## FE-COMP-001: Add loading states to all async operations
This document outlines the patterns and best practices for adding loading states to async operations in the Veza frontend.
## Components Available
### 1. LoadingSpinner
Located at `@/components/ui/loading-spinner`
```tsx
import { LoadingSpinner } from '@/components/ui/loading-spinner';
<LoadingSpinner size="md" text="Loading..." />;
```
### 2. Skeleton
Located at `@/components/ui/skeleton`
```tsx
import { Skeleton } from '@/components/ui/skeleton';
<Skeleton variant="rectangular" width="100%" height="200px" />;
```
### 3. ButtonLoading
Located at `@/components/ui/button-loading`
```tsx
import { ButtonLoading } from '@/components/ui/button-loading';
<ButtonLoading
isLoading={isSubmitting}
loadingText="Submitting..."
onClick={handleSubmit}
>
Submit
</ButtonLoading>;
```
## Patterns
### Pattern 1: Form Submissions
Always disable the submit button and show a loading indicator during submission:
```tsx
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
try {
await submitForm();
} finally {
setIsSubmitting(false);
}
};
<Button type="submit" disabled={isSubmitting}>
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Submit
</Button>;
```
### Pattern 2: Data Fetching with TanStack Query
Use `isLoading` from `useQuery`:
```tsx
const { data, isLoading, error } = useQuery({
queryKey: ['key'],
queryFn: fetchData,
});
if (isLoading) {
return <LoadingSpinner />;
}
```
### Pattern 3: Mutations with TanStack Query
Use `isPending` from `useMutation`:
```tsx
const mutation = useMutation({
mutationFn: updateData,
});
<Button onClick={() => mutation.mutate(data)} disabled={mutation.isPending}>
{mutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Update
</Button>;
```
### Pattern 4: Skeleton Loaders for Lists
Use skeleton loaders while data is loading:
```tsx
if (isLoading) {
return (
<div className="space-y-4">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} height="80px" />
))}
</div>
);
}
```
### Pattern 5: Inline Loading States
For operations that don't block the entire UI:
```tsx
<Button onClick={handleAction} disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Processing...
</>
) : (
'Action'
)}
</Button>
```
## Checklist
When implementing async operations, ensure:
- [ ] Button is disabled during operation
- [ ] Loading indicator is visible (spinner or skeleton)
- [ ] Loading text/state is clear to user
- [ ] Error states are handled
- [ ] Success states provide feedback
- [ ] Form inputs are disabled during submission (if applicable)
- [ ] Navigation is prevented during critical operations
## Examples in Codebase
- `PlaylistForm.tsx` - Form submission with loading state
- `FollowButton.tsx` - Inline loading state
- `AddCollaboratorModal.tsx` - Mutation with loading state
- `NotificationsPage.tsx` - Query with loading state
- `SearchPage.tsx` - Multiple queries with loading states