项目文件夹

文件
wehub-resource-sync bb5c75ce05
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:58 +08:00

1 行
6.7 KiB
JSON

{"content": "---\nname: react-ui-patterns\ndescription: Modern React UI patterns for loading states, error handling, and data fetching. Use when building UI components, handling async data, or managing UI states.\n---\n\n# React UI Patterns\n\n## Core Principles\n\n1. **Never show stale UI** - Loading spinners only when actually loading\n2. **Always surface errors** - Users must know when something fails\n3. **Optimistic updates** - Make the UI feel instant\n4. **Progressive disclosure** - Show content as it becomes available\n5. **Graceful degradation** - Partial data is better than no data\n\n## Loading State Patterns\n\n### The Golden Rule\n\n**Show loading indicator ONLY when there's no data to display.**\n\n```typescript\n// CORRECT - Only show loading when no data exists\nconst { data, loading, error } = useGetItemsQuery();\n\nif (error) return <ErrorState error={error} onRetry={refetch} />;\nif (loading && !data) return <LoadingState />;\nif (!data?.items.length) return <EmptyState />;\n\nreturn <ItemList items={data.items} />;\n```\n\n```typescript\n// WRONG - Shows spinner even when we have cached data\nif (loading) return <LoadingState />; // Flashes on refetch!\n```\n\n### Loading State Decision Tree\n\n```\nIs there an error?\n → Yes: Show error state with retry option\n → No: Continue\n\nIs it loading AND we have no data?\n → Yes: Show loading indicator (spinner/skeleton)\n → No: Continue\n\nDo we have data?\n → Yes, with items: Show the data\n → Yes, but empty: Show empty state\n → No: Show loading (fallback)\n```\n\n### Skeleton vs Spinner\n\n| Use Skeleton When | Use Spinner When |\n|-------------------|------------------|\n| Known content shape | Unknown content shape |\n| List/card layouts | Modal actions |\n| Initial page load | Button submissions |\n| Content placeholders | Inline operations |\n\n## Error Handling Patterns\n\n### The Error Handling Hierarchy\n\n```\n1. Inline error (field-level) → Form validation errors\n2. Toast notification → Recoverable errors, user can retry\n3. Error banner → Page-level errors, data still partially usable\n4. Full error screen → Unrecoverable, needs user action\n```\n\n### Always Show Errors\n\n**CRITICAL: Never swallow errors silently.**\n\n```typescript\n// CORRECT - Error always surfaced to user\nconst [createItem, { loading }] = useCreateItemMutation({\n onCompleted: () => {\n toast.success({ title: 'Item created' });\n },\n onError: (error) => {\n console.error('createItem failed:', error);\n toast.error({ title: 'Failed to create item' });\n },\n});\n\n// WRONG - Error silently caught, user has no idea\nconst [createItem] = useCreateItemMutation({\n onError: (error) => {\n console.error(error); // User sees nothing!\n },\n});\n```\n\n### Error State Component Pattern\n\n```typescript\ninterface ErrorStateProps {\n error: Error;\n onRetry?: () => void;\n title?: string;\n}\n\nconst ErrorState = ({ error, onRetry, title }: ErrorStateProps) => (\n <div className=\"error-state\">\n <Icon name=\"exclamation-circle\" />\n <h3>{title ?? 'Something went wrong'}</h3>\n <p>{error.message}</p>\n {onRetry && (\n <Button onClick={onRetry}>Try Again</Button>\n )}\n </div>\n);\n```\n\n## Button State Patterns\n\n### Button Loading State\n\n```tsx\n<Button\n onClick={handleSubmit}\n isLoading={isSubmitting}\n disabled={!isValid || isSubmitting}\n>\n Submit\n</Button>\n```\n\n### Disable During Operations\n\n**CRITICAL: Always disable triggers during async operations.**\n\n```tsx\n// CORRECT - Button disabled while loading\n<Button\n disabled={isSubmitting}\n isLoading={isSubmitting}\n onClick={handleSubmit}\n>\n Submit\n</Button>\n\n// WRONG - User can tap multiple times\n<Button onClick={handleSubmit}>\n {isSubmitting ? 'Submitting...' : 'Submit'}\n</Button>\n```\n\n## Empty States\n\n### Empty State Requirements\n\nEvery list/collection MUST have an empty state:\n\n```tsx\n// WRONG - No empty state\nreturn <FlatList data={items} />;\n\n// CORRECT - Explicit empty state\nreturn (\n <FlatList\n data={items}\n ListEmptyComponent={<EmptyState />}\n />\n);\n```\n\n### Contextual Empty States\n\n```tsx\n// Search with no results\n<EmptyState\n icon=\"search\"\n title=\"No results found\"\n description=\"Try different search terms\"\n/>\n\n// List with no items yet\n<EmptyState\n icon=\"plus-circle\"\n title=\"No items yet\"\n description=\"Create your first item\"\n action={{ label: 'Create Item', onClick: handleCreate }}\n/>\n```\n\n## Form Submission Pattern\n\n```tsx\nconst MyForm = () => {\n const [submit, { loading }] = useSubmitMutation({\n onCompleted: handleSuccess,\n onError: handleError,\n });\n\n const handleSubmit = async () => {\n if (!isValid) {\n toast.error({ title: 'Please fix errors' });\n return;\n }\n await submit({ variables: { input: values } });\n };\n\n return (\n <form>\n <Input\n value={values.name}\n onChange={handleChange('name')}\n error={touched.name ? errors.name : undefined}\n />\n <Button\n type=\"submit\"\n onClick={handleSubmit}\n disabled={!isValid || loading}\n isLoading={loading}\n >\n Submit\n </Button>\n </form>\n );\n};\n```\n\n## Anti-Patterns\n\n### Loading States\n\n```typescript\n// WRONG - Spinner when data exists (causes flash)\nif (loading) return <Spinner />;\n\n// CORRECT - Only show loading without data\nif (loading && !data) return <Spinner />;\n```\n\n### Error Handling\n\n```typescript\n// WRONG - Error swallowed\ntry {\n await mutation();\n} catch (e) {\n console.log(e); // User has no idea!\n}\n\n// CORRECT - Error surfaced\nonError: (error) => {\n console.error('operation failed:', error);\n toast.error({ title: 'Operation failed' });\n}\n```\n\n### Button States\n\n```typescript\n// WRONG - Button not disabled during submission\n<Button onClick={submit}>Submit</Button>\n\n// CORRECT - Disabled and shows loading\n<Button onClick={submit} disabled={loading} isLoading={loading}>\n Submit\n</Button>\n```\n\n## Checklist\n\nBefore completing any UI component:\n\n**UI States:**\n- [ ] Error state handled and shown to user\n- [ ] Loading state shown only when no data exists\n- [ ] Empty state provided for collections\n- [ ] Buttons disabled during async operations\n- [ ] Buttons show loading indicator when appropriate\n\n**Data & Mutations:**\n- [ ] Mutations have onError handler\n- [ ] All user actions have feedback (toast/visual)\n\n## Integration with Other Skills\n\n- **graphql-schema**: Use mutation patterns with proper error handling\n- **testing-patterns**: Test all UI states (loading, error, empty, success)\n- **formik-patterns**: Apply form submission patterns\n"}