Modern React UI patterns for loading states, error handling, and data fetching. Use when building UI components, handling async data, or managing UI states.
npx mdskills install sickn33/react-ui-patterns@sickn33? Sign in with GitHub to claim this listing.Comprehensive 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.