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:
- Layout Shift: Content position jumps when loading completes
- CLS (Cumulative Layout Shift): Poor Core Web Vital score
- Jarring UX: Page structure changes suddenly
- 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 messageshowError(message)- Red error messageshowWarning(message)- Orange warning messageshowInfo(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:
- component-patterns.md - Suspense integration
- data-fetching.md - useSuspenseQuery details