react-ui-patterns
π―Skillfrom sickn33/antigravity-awesome-skills
Modern React UI patterns for loading states, error handling, and data fetching. Use when building UI components, handling async data, or managing UI states.
Installation
npx skills add https://github.com/sickn33/antigravity-awesome-skills --skill react-ui-patternsSkill Details
Modern React UI patterns for loading states, error handling, and data fetching. Use when building UI components, handling async data, or managing UI states.
Overview
# 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.
```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
```
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
```
- Inline error (field-level) β Form validation errors
- Toast notification β Recoverable errors, user can retry
- Error banner β Page-level errors, data still partially usable
- 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) => (
{error.message} {onRetry && ( )} {title ?? 'Something went wrong'}
);
```
Button State Patterns
Button Loading State
```tsx
onClick={handleSubmit}
isLoading={isSubmitting}
disabled={!isValid || isSubmitting}
>
Submit
```
Disable During Operations
CRITICAL: Always disable triggers during async operations.
```tsx
// CORRECT - Button disabled while loading
disabled={isSubmitting}
isLoading={isSubmitting}
onClick={handleSubmit}
>
Submit
// WRONG - User can tap multiple times
{isSubmitting ? 'Submitting...' : 'Submit'}
```
Empty States
Empty State Requirements
Every list/collection MUST have an empty state:
```tsx
// WRONG - No empty state
return
// CORRECT - Explicit empty state
return (
data={items}
ListEmptyComponent={
/>
);
```
Contextual Empty States
```tsx
// Search with no results
icon="search"
title="No results found"
description="Try different search terms"
/>
// List with no items yet
icon="plus-circle"
title="No items yet"
description="Create your first item"
action={{ label: 'Create Item', onClick: handleCreate }}
/>
```
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 (
value={values.name}
onChange={handleChange('name')}
error={touched.name ? errors.name : undefined}
/>
type="submit"
onClick={handleSubmit}
disabled={!isValid || loading}
isLoading={loading}
>
Submit
);
};
```
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
Submit
```
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