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

3.2 KiB

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

import { LoadingSpinner } from '@/components/ui/loading-spinner';

<LoadingSpinner size="md" text="Loading..." />;

2. Skeleton

Located at @/components/ui/skeleton

import { Skeleton } from '@/components/ui/skeleton';

<Skeleton variant="rectangular" width="100%" height="200px" />;

3. ButtonLoading

Located at @/components/ui/button-loading

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:

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:

const { data, isLoading, error } = useQuery({
  queryKey: ['key'],
  queryFn: fetchData,
});

if (isLoading) {
  return <LoadingSpinner />;
}

Pattern 3: Mutations with TanStack Query

Use isPending from useMutation:

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:

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:

<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