--- name: react-ui-patterns description: 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 1. **Never show stale UI** - Loading spinners only when actually loading 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 loading indicator ONLY when there's no data to display.** ```typescript // CORRECT - Only show loading when no data exists const { data, loading, error } = useGetItemsQuery(); if (error) return ; if (loading && !data) return ; if (!data?.items.length) return ; return ; ``` ```typescript // WRONG - Shows spinner even when we have cached data if (loading) return ; // 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) => (

{title ?? 'Something went wrong'}

{error.message}

{onRetry && ( )}
); ``` ## Button State Patterns ### Button Loading State ```tsx ``` ### Disable During Operations **CRITICAL: Always disable triggers during async operations.** ```tsx // CORRECT - Button disabled while loading // WRONG - User can tap multiple times ``` ## Empty States ### Empty State Requirements Every list/collection MUST have an empty state: ```tsx // WRONG - No empty state return ; // CORRECT - Explicit empty state return ( } /> ); ``` ### Contextual Empty States ```tsx // Search with no results // List with no items yet ``` ## Form Submission Pattern ```tsx const MyForm = () => { const [submit, { loading }] = useSubmitMutation({ onCompleted: handleSuccess, onError: handleError, }); const handleSubmit = async () => { if (!isValid) { toast.error({ title: 'Please fix errors' }); return; } await submit({ variables: { input: values } }); }; return (
); }; ``` ## Anti-Patterns ### Loading States ```typescript // WRONG - Spinner when data exists (causes flash) if (loading) return ; // CORRECT - Only show loading without data if (loading && !data) return ; ``` ### Error Handling ```typescript // WRONG - Error swallowed try { await mutation(); } catch (e) { console.log(e); // User has no idea! } // CORRECT - Error surfaced onError: (error) => { console.error('operation failed:', error); toast.error({ title: 'Operation failed' }); } ``` ### Button States ```typescript // WRONG - Button not disabled during submission // CORRECT - Disabled and shows loading ``` ## Checklist Before completing any UI component: **UI States:** - [ ] Error state handled and shown to user - [ ] Loading state shown only when no data exists - [ ] Empty state provided for collections - [ ] Buttons disabled during async operations - [ ] Buttons show loading indicator when appropriate **Data & Mutations:** - [ ] Mutations have onError handler - [ ] All user actions have feedback (toast/visual) ## Integration with Other Skills - **graphql-schema**: Use mutation patterns with proper error handling - **testing-patterns**: Test all UI states (loading, error, empty, success) - **formik-patterns**: Apply form submission patterns