- Created skills/ directory - Moved 272 skills to skills/ subfolder - Kept agents/ at root level - Kept installation scripts and docs at root level Repository structure: - skills/ - All 272 skills from skills.sh - agents/ - Agent definitions - *.sh, *.ps1 - Installation scripts - README.md, etc. - Documentation Co-Authored-By: Claude <noreply@anthropic.com>
290 lines
6.3 KiB
Markdown
290 lines
6.3 KiB
Markdown
---
|
|
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 <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 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 (
|
|
<form>
|
|
<Input
|
|
value={values.name}
|
|
onChange={handleChange('name')}
|
|
error={touched.name ? errors.name : undefined}
|
|
/>
|
|
<Button
|
|
type="submit"
|
|
onClick={handleSubmit}
|
|
disabled={!isValid || loading}
|
|
isLoading={loading}
|
|
>
|
|
Submit
|
|
</Button>
|
|
</form>
|
|
);
|
|
};
|
|
```
|
|
|
|
## Anti-Patterns
|
|
|
|
### Loading States
|
|
|
|
```typescript
|
|
// WRONG - Spinner when data exists (causes flash)
|
|
if (loading) return <Spinner />;
|
|
|
|
// CORRECT - Only show loading without data
|
|
if (loading && !data) return <Spinner />;
|
|
```
|
|
|
|
### 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
|
|
<Button onClick={submit}>Submit</Button>
|
|
|
|
// CORRECT - Disabled and shows loading
|
|
<Button onClick={submit} disabled={loading} isLoading={loading}>
|
|
Submit
|
|
</Button>
|
|
```
|
|
|
|
## 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
|