2025-12-24 12:25:10 +00:00
|
|
|
# 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
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-24 12:25:10 +00:00
|
|
|
Located at `@/components/ui/loading-spinner`
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
|
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
<LoadingSpinner size="md" text="Loading..." />;
|
2025-12-24 12:25:10 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 2. Skeleton
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-24 12:25:10 +00:00
|
|
|
Located at `@/components/ui/skeleton`
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
<Skeleton variant="rectangular" width="100%" height="200px" />;
|
2025-12-24 12:25:10 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 3. ButtonLoading
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-24 12:25:10 +00:00
|
|
|
Located at `@/components/ui/button-loading`
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
import { ButtonLoading } from '@/components/ui/button-loading';
|
|
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
<ButtonLoading
|
|
|
|
|
isLoading={isSubmitting}
|
2025-12-24 12:25:10 +00:00
|
|
|
loadingText="Submitting..."
|
|
|
|
|
onClick={handleSubmit}
|
|
|
|
|
>
|
|
|
|
|
Submit
|
2026-01-13 18:47:57 +00:00
|
|
|
</ButtonLoading>;
|
2025-12-24 12:25:10 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
<Button type="submit" disabled={isSubmitting}>
|
2025-12-24 12:25:10 +00:00
|
|
|
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
|
|
|
Submit
|
2026-01-13 18:47:57 +00:00
|
|
|
</Button>;
|
2025-12-24 12:25:10 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 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,
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
<Button onClick={() => mutation.mutate(data)} disabled={mutation.isPending}>
|
2025-12-24 12:25:10 +00:00
|
|
|
{mutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
|
|
|
Update
|
2026-01-13 18:47:57 +00:00
|
|
|
</Button>;
|
2025-12-24 12:25:10 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 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
|