Modern React UI patterns for loading states, error handling, and data fetching. Use when building UI components, handling async data, or managing UI states.
Add this skill
npx mdskills install sickn33/react-ui-patternsComprehensive React UI patterns with clear decision trees and anti-pattern examples
1---2name: react-ui-patterns3description: Modern React UI patterns for loading states, error handling, and data fetching. Use when building UI components, handling async data, or managing UI states.4---56# React UI Patterns78## Core Principles9101. **Never show stale UI** - Loading spinners only when actually loading112. **Always surface errors** - Users must know when something fails123. **Optimistic updates** - Make the UI feel instant134. **Progressive disclosure** - Show content as it becomes available145. **Graceful degradation** - Partial data is better than no data1516## Loading State Patterns1718### The Golden Rule1920**Show loading indicator ONLY when there's no data to display.**2122```typescript23// CORRECT - Only show loading when no data exists24const { data, loading, error } = useGetItemsQuery();2526if (error) return <ErrorState error={error} onRetry={refetch} />;27if (loading && !data) return <LoadingState />;28if (!data?.items.length) return <EmptyState />;2930return <ItemList items={data.items} />;31```3233```typescript34// WRONG - Shows spinner even when we have cached data35if (loading) return <LoadingState />; // Flashes on refetch!36```3738### Loading State Decision Tree3940```41Is there an error?42 → Yes: Show error state with retry option43 → No: Continue4445Is it loading AND we have no data?46 → Yes: Show loading indicator (spinner/skeleton)47 → No: Continue4849Do we have data?50 → Yes, with items: Show the data51 → Yes, but empty: Show empty state52 → No: Show loading (fallback)53```5455### Skeleton vs Spinner5657| Use Skeleton When | Use Spinner When |58|-------------------|------------------|59| Known content shape | Unknown content shape |60| List/card layouts | Modal actions |61| Initial page load | Button submissions |62| Content placeholders | Inline operations |6364## Error Handling Patterns6566### The Error Handling Hierarchy6768```691. Inline error (field-level) → Form validation errors702. Toast notification → Recoverable errors, user can retry713. Error banner → Page-level errors, data still partially usable724. Full error screen → Unrecoverable, needs user action73```7475### Always Show Errors7677**CRITICAL: Never swallow errors silently.**7879```typescript80// CORRECT - Error always surfaced to user81const [createItem, { loading }] = useCreateItemMutation({82 onCompleted: () => {83 toast.success({ title: 'Item created' });84 },85 onError: (error) => {86 console.error('createItem failed:', error);87 toast.error({ title: 'Failed to create item' });88 },89});9091// WRONG - Error silently caught, user has no idea92const [createItem] = useCreateItemMutation({93 onError: (error) => {94 console.error(error); // User sees nothing!95 },96});97```9899### Error State Component Pattern100101```typescript102interface ErrorStateProps {103 error: Error;104 onRetry?: () => void;105 title?: string;106}107108const ErrorState = ({ error, onRetry, title }: ErrorStateProps) => (109 <div className="error-state">110 <Icon name="exclamation-circle" />111 <h3>{title ?? 'Something went wrong'}</h3>112 <p>{error.message}</p>113 {onRetry && (114 <Button onClick={onRetry}>Try Again</Button>115 )}116 </div>117);118```119120## Button State Patterns121122### Button Loading State123124```tsx125<Button126 onClick={handleSubmit}127 isLoading={isSubmitting}128 disabled={!isValid || isSubmitting}129>130 Submit131</Button>132```133134### Disable During Operations135136**CRITICAL: Always disable triggers during async operations.**137138```tsx139// CORRECT - Button disabled while loading140<Button141 disabled={isSubmitting}142 isLoading={isSubmitting}143 onClick={handleSubmit}144>145 Submit146</Button>147148// WRONG - User can tap multiple times149<Button onClick={handleSubmit}>150 {isSubmitting ? 'Submitting...' : 'Submit'}151</Button>152```153154## Empty States155156### Empty State Requirements157158Every list/collection MUST have an empty state:159160```tsx161// WRONG - No empty state162return <FlatList data={items} />;163164// CORRECT - Explicit empty state165return (166 <FlatList167 data={items}168 ListEmptyComponent={<EmptyState />}169 />170);171```172173### Contextual Empty States174175```tsx176// Search with no results177<EmptyState178 icon="search"179 title="No results found"180 description="Try different search terms"181/>182183// List with no items yet184<EmptyState185 icon="plus-circle"186 title="No items yet"187 description="Create your first item"188 action={{ label: 'Create Item', onClick: handleCreate }}189/>190```191192## Form Submission Pattern193194```tsx195const MyForm = () => {196 const [submit, { loading }] = useSubmitMutation({197 onCompleted: handleSuccess,198 onError: handleError,199 });200201 const handleSubmit = async () => {202 if (!isValid) {203 toast.error({ title: 'Please fix errors' });204 return;205 }206 await submit({ variables: { input: values } });207 };208209 return (210 <form>211 <Input212 value={values.name}213 onChange={handleChange('name')}214 error={touched.name ? errors.name : undefined}215 />216 <Button217 type="submit"218 onClick={handleSubmit}219 disabled={!isValid || loading}220 isLoading={loading}221 >222 Submit223 </Button>224 </form>225 );226};227```228229## Anti-Patterns230231### Loading States232233```typescript234// WRONG - Spinner when data exists (causes flash)235if (loading) return <Spinner />;236237// CORRECT - Only show loading without data238if (loading && !data) return <Spinner />;239```240241### Error Handling242243```typescript244// WRONG - Error swallowed245try {246 await mutation();247} catch (e) {248 console.log(e); // User has no idea!249}250251// CORRECT - Error surfaced252onError: (error) => {253 console.error('operation failed:', error);254 toast.error({ title: 'Operation failed' });255}256```257258### Button States259260```typescript261// WRONG - Button not disabled during submission262<Button onClick={submit}>Submit</Button>263264// CORRECT - Disabled and shows loading265<Button onClick={submit} disabled={loading} isLoading={loading}>266 Submit267</Button>268```269270## Checklist271272Before completing any UI component:273274**UI States:**275- [ ] Error state handled and shown to user276- [ ] Loading state shown only when no data exists277- [ ] Empty state provided for collections278- [ ] Buttons disabled during async operations279- [ ] Buttons show loading indicator when appropriate280281**Data & Mutations:**282- [ ] Mutations have onError handler283- [ ] All user actions have feedback (toast/visual)284285## Integration with Other Skills286287- **graphql-schema**: Use mutation patterns with proper error handling288- **testing-patterns**: Test all UI states (loading, error, empty, success)289- **formik-patterns**: Apply form submission patterns290
Full transparency — inspect the skill content before installing.