All skills
Skillintermediate

Loading & Error States

**CRITICAL**: Proper loading and error state handling prevents layout shift and provides better user experience.

Claude Code Knowledge Pack7/10/2026

Overview

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

// ❌ 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)


const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

    return (
        
            
        
    );
};

Option 2: LoadingOverlay (for legacy useQuery patterns)


    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


// Or

Basic Usage


    

With useSuspenseQuery


const Inner: React.FC = () => {
    // No isLoading needed!
    const { data } = useSuspenseQuery({
        queryKey: ['data'],
        queryFn: () => api.getData(),
    });

    return ;
};

// Outer component wraps in Suspense

    return (
        
            
        
    );
};

Multiple Suspense Boundaries

Pattern: Separate loading for independent sections


    return (
        
            
                
            

            
                
            

            
                
            
        
    );
};

Benefits:

  • Each section loads independently
  • User sees partial content sooner
  • Better perceived performance

Nested Suspense


    return (
        
            
            
                
                    
                    
                
            
        
    );
};

LoadingOverlay Component

When to Use

  • Legacy components with useQuery (not refactored to Suspense yet)
  • Overlay loading state needed
  • Can't use Suspense boundaries

Usage


    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


    const { showSuccess, showError, showInfo, showWarning } = useMuiSnackbar();

    const handleAction = async () => {
        try {
            await api.doSomething();
            showSuccess('Operation completed successfully');
        } catch (error) {
            showError('Operation failed');
        }
    };

    return Do Action;
};

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


    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


function ErrorFallback({ error, resetErrorBoundary }) {
    return (
        
            
                Something went wrong
            
            {error.message}
            Try Again
        
    );
}

    return (
         console.error('Boundary caught:', error)}
        >
            
                
            
        
    );
};

Complete Examples

Example 1: Modern Component with Suspense


// 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 (
        
            <h2>{data.title}</h2>
            <p>{data.description}</p>
        
    );
};

// Outer component provides Suspense boundary

    return (
        
            
                
            
        
    );
};

Example 2: Legacy Pattern with LoadingOverlay


    const { data, isLoading, error } = useQuery({
        queryKey: ['entity', id],
        queryFn: () => myFeatureApi.getEntity(id),
    });

    return (
        
            
                {error && }
                {data && }
            
        
    );
};

Example 3: Error Handling with Snackbar


    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 (
         updateMutation.mutate({ name: 'New' })}>
            Update
        
    );
};

Loading State Anti-Patterns

❌ What NOT to Do

// ❌ NEVER - Early return
if (isLoading) {
    return ;
}

// ❌ NEVER - Conditional rendering
{isLoading ?  : }

// ❌ NEVER - Layout changes
if (isLoading) {
    return (
        
            
        
    );
}
return (
      // Different height!
        
    
);

✅ What TO Do

// ✅ BEST - useSuspenseQuery + SuspenseLoader

    

// ✅ ACCEPTABLE - LoadingOverlay

    

// ✅ OK - Inline skeleton with same layout

    {isLoading ?  : }


Skeleton Loading (Alternative)

MUI Skeleton Component


    const { data, isLoading } = useQuery({ ... });

    return (
        
            {isLoading ? (
                <>
                    
                    
                    
                </>
            ) : (
                <>
                    {data.title}
                    <img src={data.image} />
                    {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: