React UI Patterns
/SKILL// CORRECT - Only show loading when no data exists
--- name: react-ui-patterns description: Modern React UI patterns for loading states, error handling, and data fetching. Use when building UI components, handling asynchronous data, or managing UI states. --- # React UI Patterns ## Core Principles 1. Never show stale UI - Only display loading spinners when data is actually being loaded 2. Always surface errors - Users must know when something fails 3. Optimistic updates - Make the UI feel instant 4. Progressive disclosure - Show content as it becomes available 5. Graceful degradation - Partial data is better than no data ## Loading State Patterns ### The Golden Rule Show a loading indicator ONLY when there is no data to display. ``typescript // CORRECT - Only show loading when no data exists const { data, loading, error } = useGetItemsQuery(); if (error) return <ErrorState error={error} onRetry={refetch} />; if (loading && !data) return <LoadingState />; if (!data?.items.length) return <EmptyState />; return <ItemList items={data.items} />; ` `typescript // WRONG - Shows spinner even when we have cached data if (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.** `typescript // CORRECT - Error always surfaced to user const [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 idea const [createItem] = useCreateItemMutation({ onError: (error) => { console.error(error); // User sees nothing! }, }); ` ### Error State Component Pattern `typescript interface ErrorStateProps { error: Error; onRetry?: () => void; title?: string; } const ErrorState = ({ error, onRetry, title }: ErrorStateProps) => ( <div className="error-state"> <Icon name="exclamation-circle" /> <h3>{title ?? 'Something went wrong'}</h3> <p>{error.message}</p> {onRetry && ( <Button onClick={onRetry}>Try Again</Button> )} </div> ); ` ## Button State Patterns ### Button Loading State `tsx <Button onClick={handleSubmit} isLoading={isSubmitting} disabled={!isValid || isSubmitting} > Submit </Button> ` ### Disable During Operations **CRITICAL: Always disable triggers during async operations.** `tsx // CORRECT - Button disabled while loading <Button disabled={isSubmitting} isLoading={isSubmitting} onClick={handleSubmit} > Submit </Button> // WRONG - User can tap multiple times <Button onClick={handleSubmit}> {isSubmitting ? 'Submitting...' : 'Submit'} </Button> ` ## Empty States ### Empty State Requirements Every list/collection MUST have an empty state: `tsx // WRONG - No empty state return <FlatList data={items} />; // CORRECT - Explicit empty state return ( <FlatList data={items} ListEmptyComponent={<EmptyState />} /> ); ` ### Contextual Empty States ``tsx // Search with no results <EmptyState icon="search" title="No results found" description="Try different search terms" /> // List with no items yet <EmptyState icon="plus-circle" title="No it