react-ui-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React UI Patterns

React UI 模式

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
  1. 绝不显示过时UI - 仅在实际加载时显示加载动画
  2. 始终暴露错误 - 用户必须知晓何时出现故障
  3. 乐观更新 - 让UI体验更流畅即时
  4. 渐进式披露 - 内容就绪时再展示
  5. 优雅降级 - 部分数据优于无数据

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!
仅在无数据可展示时显示加载指示器。
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

骨架屏 vs 加载动画

Use Skeleton WhenUse Spinner When
Known content shapeUnknown content shape
List/card layoutsModal actions
Initial page loadButton submissions
Content placeholdersInline 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
1. 内联错误(字段级)→ 表单验证错误
2. 提示通知(Toast)→ 可恢复错误,用户可重试
3. 错误横幅 → 页面级错误,数据仍可部分使用
4. 全屏错误页 → 不可恢复,需用户操作

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!
  },
});
重要提示:绝不能静默忽略错误。
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>
);
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>
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>
重要提示:异步操作期间必须禁用触发按钮。
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 />}
  />
);
每个列表/集合都必须设置空状态:
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 }}
/>
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>
  );
};
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 />;
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' });
}
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>
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)
完成任何UI组件前:
UI状态:
  • 错误状态已处理并展示给用户
  • 仅在无数据时显示加载状态
  • 为集合设置空状态
  • 异步操作期间禁用按钮
  • 按钮在合适时机显示加载指示器
数据与突变:
  • 突变操作包含onError处理函数
  • 所有用户操作都有反馈(提示/视觉反馈)

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
  • graphql-schema:结合正确的错误处理使用突变模式
  • testing-patterns:测试所有UI状态(加载、错误、空、成功)
  • formik-patterns:应用表单提交模式