Modern React UI patterns for loading states, error handling, and data fetching. Use when building UI components, handling async data, or managing UI states.
Modern React UI patterns for loading states, error handling, and data fetching. Use when building UI components, handling async data, or managing UI states.
React UI Patterns
Core Principles
Never show stale UI - Loading spinners only when actually loading
Always surface errors - Users must know when something fails
Optimistic updates - Make the UI feel instant
Progressive disclosure - Show content as it becomes available
Graceful degradation - Partial data is better than no data
Loading State Patterns
The Golden Rule
Show loading indicator ONLY when there's no data to display.
// CORRECT - Only show loading when no data existsconst { data, loading, error } = useGetItemsQuery();
if (error) return<ErrorStateerror={error}onRetry={refetch} />;
if (loading && !data) return<LoadingState />;
if (!data?.items.length) return< />;
;
EmptyState
return
<ItemListitems={data.items} />
// WRONG - Shows spinner even when we have cached dataif (loading) return<LoadingState />; // Flashes on refetch!
Loading State Decision Tree
Is there an error?
→ Yes: Show error state with retry option
→ No: Continue
Is it loading AND we have no data?
→ Yes: Show loading indicator (spinner/skeleton)
→ No: Continue
Do we have data?
→ Yes, with items: Show the data
→ Yes, but empty: Show empty state
→ No: Show loading (fallback)
Skeleton vs Spinner
Use Skeleton When
Use Spinner When
Known content shape
Unknown content shape
List/card layouts
Modal actions
Initial page load
Button submissions
Content placeholders
Inline operations
Error Handling Patterns
The Error Handling Hierarchy
1. Inline error (field-level) → Form validation errors
2. Toast notification → Recoverable errors, user can retry
3. Error banner → Page-level errors, data still partially usable
4. Full error screen → Unrecoverable, needs user action
Always Show Errors
CRITICAL: Never swallow errors silently.
// CORRECT - Error always surfaced to userconst [createItem, { loading }] = useCreateItemMutation({
onCompleted: () => {
toast.success({ title: 'Item created' });
},
onError: (error) => {
console.error('createItem failed:', error);
toast.error({ title: 'Failed to create item' });
},
});
// WRONG - Error silently caught, user has no ideaconst [createItem] = useCreateItemMutation({
onError: (error) => {
console.error(error); // User sees nothing!
},
});
// Search with no results
<EmptyState
icon="search"
title="No results found"
description="Try different search terms"
/>
// List with no items yet<EmptyStateicon="plus-circle"title="No items yet"description="Create your first item"action={{label: 'CreateItem', onClick:handleCreate }}
/>
// WRONG - Spinner when data exists (causes flash)if (loading) return<Spinner />;
// CORRECT - Only show loading without dataif (loading && !data) return<Spinner />;