Skip to content

组件设计原则 - React组件架构最佳实践

本文档详细阐述React组件设计的核心原则与最佳实践,包括组件拆分、职责划分、复用性设计、可测试性等方面的指导。

1. SOLID原则在React中的应用

1.1 单一职责原则 (Single Responsibility Principle)

原则: 一个组件应该只有一个改变的理由

tsx
// ❌ 违反单一职责: 组件做了太多事情
function UserDashboard() {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [notifications, setNotifications] = useState([]);
  const [settings, setSettings] = useState({});
  
  useEffect(() => {
    // 获取用户信息
    fetchUser().then(setUser);
    // 获取帖子
    fetchPosts().then(setPosts);
    // 获取通知
    fetchNotifications().then(setNotifications);
    // 获取设置
    fetchSettings().then(setSettings);
  }, []);
  
  const handleLogout = () => { /* ... */ };
  const handleUpdateProfile = () => { /* ... */ };
  const handleDeletePost = () => { /* ... */ };
  const handleMarkAsRead = () => { /* ... */ };
  
  return (
    <div>
      <UserProfile user={user} onUpdate={handleUpdateProfile} onLogout={handleLogout} />
      <PostList posts={posts} onDelete={handleDeletePost} />
      <NotificationList notifications={notifications} onMarkAsRead={handleMarkAsRead} />
      <Settings settings={settings} />
    </div>
  );
}

// ✅ 遵循单一职责: 拆分为多个职责单一的组件
function UserDashboard() {
  return (
    <div>
      <UserProfileSection />
      <PostListSection />
      <NotificationSection />
      <SettingsSection />
    </div>
  );
}

function UserProfileSection() {
  const { user, loading, error, updateProfile, logout } = useUserProfile();
  
  if (loading) return <Skeleton />;
  if (error) return <Error error={error} />;
  
  return <UserProfile user={user} onUpdate={updateProfile} onLogout={logout} />;
}

function PostListSection() {
  const { posts, loading, deletePost } = usePosts();
  
  if (loading) return <Skeleton />;
  
  return <PostList posts={posts} onDelete={deletePost} />;
}

function NotificationSection() {
  const { notifications, markAsRead } = useNotifications();
  
  return <NotificationList notifications={notifications} onMarkAsRead={markAsRead} />;
}

function SettingsSection() {
  const { settings, updateSettings } = useSettings();
  
  return <Settings settings={settings} onUpdate={updateSettings} />;
}

1.2 开闭原则 (Open/Closed Principle)

原则: 对扩展开放,对修改关闭

tsx
// ❌ 违反开闭原则: 添加新类型需要修改组件
function Alert({ type, message }: { type: string; message: string }) {
  let icon;
  let color;
  
  if (type === 'success') {
    icon = <CheckIcon />;
    color = 'green';
  } else if (type === 'error') {
    icon = <ErrorIcon />;
    color = 'red';
  } else if (type === 'warning') {
    icon = <WarningIcon />;
    color = 'yellow';
  } else if (type === 'info') {
    icon = <InfoIcon />;
    color = 'blue';
  }
  
  return (
    <div style={{ color }}>
      {icon}
      <span>{message}</span>
    </div>
  );
}

// ✅ 遵循开闭原则: 使用配置扩展
const ALERT_CONFIGS = {
  success: { icon: CheckIcon, color: 'green' },
  error: { icon: ErrorIcon, color: 'red' },
  warning: { icon: WarningIcon, color: 'yellow' },
  info: { icon: InfoIcon, color: 'blue' }
};

function Alert({ type, message }: { type: keyof typeof ALERT_CONFIGS; message: string }) {
  const config = ALERT_CONFIGS[type];
  const Icon = config.icon;
  
  return (
    <div style={{ color: config.color }}>
      <Icon />
      <span>{message}</span>
    </div>
  );
}

// ✅ 更好: 通过props扩展
interface AlertProps {
  icon: React.ComponentType;
  color: string;
  message: string;
}

function Alert({ icon: Icon, color, message }: AlertProps) {
  return (
    <div style={{ color }}>
      <Icon />
      <span>{message}</span>
    </div>
  );
}

// 使用
<Alert icon={CheckIcon} color="green" message="Success!" />

1.3 里氏替换原则 (Liskov Substitution Principle)

原则: 子组件应该可以替换父组件而不影响程序正确性

tsx
// 基础Button组件
interface ButtonProps {
  children: React.ReactNode;
  onClick?: () => void;
  disabled?: boolean;
}

function Button({ children, onClick, disabled }: ButtonProps) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {children}
    </button>
  );
}

// ✅ IconButton继承Button的所有行为
interface IconButtonProps extends ButtonProps {
  icon: React.ComponentType;
}

function IconButton({ icon: Icon, children, ...props }: IconButtonProps) {
  return (
    <Button {...props}>
      <Icon />
      {children}
    </Button>
  );
}

// ✅ LoadingButton也完全兼容Button
interface LoadingButtonProps extends ButtonProps {
  loading?: boolean;
}

function LoadingButton({ loading, children, disabled, ...props }: LoadingButtonProps) {
  return (
    <Button {...props} disabled={disabled || loading}>
      {loading ? <Spinner /> : children}
    </Button>
  );
}

// 任何使用Button的地方都可以替换为IconButton或LoadingButton
function Form() {
  return (
    <div>
      <Button onClick={() => {}}>Submit</Button>
      <IconButton icon={SaveIcon} onClick={() => {}}>Save</IconButton>
      <LoadingButton loading={true} onClick={() => {}}>Loading...</LoadingButton>
    </div>
  );
}

1.4 接口隔离原则 (Interface Segregation Principle)

原则: 不应该强迫组件依赖它不需要的props

tsx
// ❌ 违反接口隔离: 过多的props
interface UserCardProps {
  user: User;
  onEdit: () => void;
  onDelete: () => void;
  onFollow: () => void;
  onUnfollow: () => void;
  onMessage: () => void;
  onBlock: () => void;
  onReport: () => void;
  showEditButton: boolean;
  showDeleteButton: boolean;
  showFollowButton: boolean;
  showMessageButton: boolean;
}

// ✅ 遵循接口隔离: 拆分为更小的接口
interface BaseUserCardProps {
  user: User;
}

interface UserCardWithActionsProps extends BaseUserCardProps {
  actions?: {
    onEdit?: () => void;
    onDelete?: () => void;
    onFollow?: () => void;
    onMessage?: () => void;
  };
}

function UserCard({ user, actions }: UserCardWithActionsProps) {
  return (
    <div>
      <UserAvatar src={user.avatar} />
      <UserName>{user.name}</UserName>
      {actions && <UserActions actions={actions} />}
    </div>
  );
}

// 或者使用组合
function UserCard({ user, children }: BaseUserCardProps & { children?: React.ReactNode }) {
  return (
    <div>
      <UserAvatar src={user.avatar} />
      <UserName>{user.name}</UserName>
      {children}
    </div>
  );
}

// 使用
<UserCard user={user}>
  <Button onClick={onEdit}>Edit</Button>
  <Button onClick={onDelete}>Delete</Button>
</UserCard>

1.5 依赖倒置原则 (Dependency Inversion Principle)

原则: 依赖抽象而不是具体实现

tsx
// ❌ 违反依赖倒置: 直接依赖具体实现
function UserList() {
  const [users, setUsers] = useState([]);
  
  useEffect(() => {
    // 直接依赖fetch实现
    fetch('/api/users')
      .then(res => res.json())
      .then(setUsers);
  }, []);
  
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// ✅ 遵循依赖倒置: 依赖抽象接口
interface DataFetcher<T> {
  fetch: () => Promise<T>;
}

interface UserListProps {
  fetcher: DataFetcher<User[]>;
}

function UserList({ fetcher }: UserListProps) {
  const [users, setUsers] = useState<User[]>([]);
  
  useEffect(() => {
    fetcher.fetch().then(setUsers);
  }, [fetcher]);
  
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// 使用
const apiFetcher: DataFetcher<User[]> = {
  fetch: () => fetch('/api/users').then(res => res.json())
};

<UserList fetcher={apiFetcher} />

// 或者使用Hook抽象
function useUsers() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetchUsers().then(data => {
      setUsers(data);
      setLoading(false);
    });
  }, []);
  
  return { users, loading };
}

function UserList() {
  const { users, loading } = useUsers();
  
  if (loading) return <Skeleton />;
  
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

2. 组件设计模式

2.1 容器组件与展示组件

tsx
// 展示组件 (Presentational Component)
// 职责: 只负责UI渲染
// 特点: 无状态、通过props接收数据、纯函数
interface ProductCardProps {
  product: Product;
  onAddToCart: (product: Product) => void;
  onViewDetails: (id: string) => void;
}

function ProductCard({ product, onAddToCart, onViewDetails }: ProductCardProps) {
  return (
    <div>
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>{product.price}</p>
      <button onClick={() => onAddToCart(product)}>Add to Cart</button>
      <button onClick={() => onViewDetails(product.id)}>Details</button>
    </div>
  );
}

// 容器组件 (Container Component)
// 职责: 处理数据和业务逻辑
// 特点: 有状态、调用API、传递数据给展示组件
function ProductCardContainer({ productId }: { productId: string }) {
  const { product, loading, error } = useProduct(productId);
  const { addToCart } = useCart();
  const navigate = useNavigate();
  
  if (loading) return <Skeleton />;
  if (error) return <Error error={error} />;
  if (!product) return null;
  
  const handleAddToCart = (product: Product) => {
    addToCart(product);
    toast.success('Added to cart!');
  };
  
  const handleViewDetails = (id: string) => {
    navigate(`/products/${id}`);
  };
  
  return (
    <ProductCard
      product={product}
      onAddToCart={handleAddToCart}
      onViewDetails={handleViewDetails}
    />
  );
}

2.2 复合组件模式 (Compound Components)

tsx
// 使用React Context实现复合组件
const TabsContext = createContext<{
  activeTab: string;
  setActiveTab: (tab: string) => void;
} | null>(null);

function Tabs({ children, defaultTab }: { children: React.ReactNode; defaultTab: string }) {
  const [activeTab, setActiveTab] = useState(defaultTab);
  
  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div>{children}</div>
    </TabsContext.Provider>
  );
}

function TabList({ children }: { children: React.ReactNode }) {
  return <div role="tablist">{children}</div>;
}

function Tab({ id, children }: { id: string; children: React.ReactNode }) {
  const context = useContext(TabsContext);
  if (!context) throw new Error('Tab must be used within Tabs');
  
  const { activeTab, setActiveTab } = context;
  
  return (
    <button
      role="tab"
      aria-selected={activeTab === id}
      onClick={() => setActiveTab(id)}
    >
      {children}
    </button>
  );
}

function TabPanels({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>;
}

function TabPanel({ id, children }: { id: string; children: React.ReactNode }) {
  const context = useContext(TabsContext);
  if (!context) throw new Error('TabPanel must be used within Tabs');
  
  const { activeTab } = context;
  
  if (activeTab !== id) return null;
  
  return <div role="tabpanel">{children}</div>;
}

// 组合使用
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panels = TabPanels;
Tabs.Panel = TabPanel;

// 使用
function App() {
  return (
    <Tabs defaultTab="tab1">
      <Tabs.List>
        <Tabs.Tab id="tab1">Tab 1</Tabs.Tab>
        <Tabs.Tab id="tab2">Tab 2</Tabs.Tab>
        <Tabs.Tab id="tab3">Tab 3</Tabs.Tab>
      </Tabs.List>
      
      <Tabs.Panels>
        <Tabs.Panel id="tab1">Content 1</Tabs.Panel>
        <Tabs.Panel id="tab2">Content 2</Tabs.Panel>
        <Tabs.Panel id="tab3">Content 3</Tabs.Panel>
      </Tabs.Panels>
    </Tabs>
  );
}

2.3 Render Props模式

tsx
// Render Props组件
interface MouseTrackerProps {
  render: (position: { x: number; y: number }) => React.ReactNode;
}

function MouseTracker({ render }: MouseTrackerProps) {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };
    
    window.addEventListener('mousemove', handleMouseMove);
    
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []);
  
  return <>{render(position)}</>;
}

// 使用
function App() {
  return (
    <MouseTracker
      render={({ x, y }) => (
        <div>
          Mouse position: ({x}, {y})
        </div>
      )}
    />
  );
}

// 或者使用children作为函数
interface DataProviderProps<T> {
  data: T;
  children: (data: T) => React.ReactNode;
}

function DataProvider<T>({ data, children }: DataProviderProps<T>) {
  return <>{children(data)}</>;
}

// 使用
<DataProvider data={users}>
  {(users) => (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )}
</DataProvider>

2.4 高阶组件 (HOC)

tsx
// HOC: withAuth
function withAuth<P extends object>(Component: React.ComponentType<P>) {
  return function AuthenticatedComponent(props: P) {
    const { user, loading } = useAuth();
    
    if (loading) {
      return <Spinner />;
    }
    
    if (!user) {
      return <Navigate to="/login" />;
    }
    
    return <Component {...props} />;
  };
}

// HOC: withLoading
interface WithLoadingProps {
  loading: boolean;
}

function withLoading<P extends object>(
  Component: React.ComponentType<P>
) {
  return function WithLoadingComponent(props: P & WithLoadingProps) {
    const { loading, ...rest } = props;
    
    if (loading) {
      return <Spinner />;
    }
    
    return <Component {...(rest as P)} />;
  };
}

// 使用
const AuthenticatedDashboard = withAuth(Dashboard);
const DashboardWithLoading = withLoading(Dashboard);

// 组合HOC
const EnhancedDashboard = withAuth(withLoading(Dashboard));

2.5 Hooks组合模式

tsx
// 组合多个Hooks创建复杂逻辑
function useUserData(userId: string) {
  const { user, loading: userLoading, error: userError } = useUser(userId);
  const { posts, loading: postsLoading } = usePosts(userId);
  const { followers } = useFollowers(userId);
  
  const loading = userLoading || postsLoading;
  const error = userError;
  
  return {
    user,
    posts,
    followers,
    loading,
    error,
    hasData: !loading && !error && user !== null
  };
}

// 使用组合Hook
function UserProfile({ userId }: { userId: string }) {
  const { user, posts, followers, loading, error, hasData } = useUserData(userId);
  
  if (loading) return <Skeleton />;
  if (error) return <Error error={error} />;
  if (!hasData) return <NotFound />;
  
  return (
    <div>
      <UserInfo user={user} />
      <PostList posts={posts} />
      <FollowerList followers={followers} />
    </div>
  );
}

3. 组件拆分策略

3.1 何时拆分组件

tsx
// ❌ 过大的组件 (应该拆分)
function ProductPage() {
  // 数百行代码...
  // 职责: 产品信息、评论、推荐、购物车等
  return (
    <div>
      {/* 大量JSX */}
    </div>
  );
}

// ✅ 拆分后的组件
function ProductPage({ productId }: { productId: string }) {
  return (
    <div>
      <ProductHeader productId={productId} />
      <ProductDetails productId={productId} />
      <ProductReviews productId={productId} />
      <RecommendedProducts productId={productId} />
    </div>
  );
}

3.2 拆分原则

typescript
const componentSplittingPrinciples = {
  按职责拆分: '每个组件只负责一个功能',
  按可复用性拆分: '提取可复用的UI元素',
  按数据流拆分: '根据数据源和流向拆分',
  按性能拆分: '隔离频繁更新的部分',
  按测试拆分: '更小的组件更易测试',
  避免过度拆分: '不要为了拆分而拆分'
};

4. Props设计原则

4.1 Props命名

tsx
// ✅ 良好的props命名
interface ButtonProps {
  // 布尔值使用is/has/can等前缀
  isLoading?: boolean;
  isDisabled?: boolean;
  hasIcon?: boolean;
  
  // 事件处理器使用on前缀
  onClick?: () => void;
  onSubmit?: (data: FormData) => void;
  onChange?: (value: string) => void;
  
  // 回调函数使用具体动词
  onUserSelect?: (user: User) => void;
  onItemDelete?: (id: string) => void;
  
  // 内容使用children或具体名称
  children?: React.ReactNode;
  header?: React.ReactNode;
  footer?: React.ReactNode;
  
  // 样式相关使用className/style
  className?: string;
  style?: React.CSSProperties;
}

4.2 Props解构

tsx
// ✅ 解构props提高可读性
function Button({ 
  children, 
  onClick, 
  isLoading = false, 
  isDisabled = false,
  className = '',
  ...rest 
}: ButtonProps) {
  return (
    <button
      onClick={onClick}
      disabled={isDisabled || isLoading}
      className={`btn ${className}`}
      {...rest}
    >
      {isLoading ? <Spinner /> : children}
    </button>
  );
}

4.3 Props传递

tsx
// ✅ 使用spread operator传递props
function CustomInput(props: InputProps) {
  return (
    <div>
      <label>{props.label}</label>
      <input {...props} />
    </div>
  );
}

// ✅ 选择性传递props
function CustomButton({ 
  customProp, 
  ...buttonProps 
}: CustomButtonProps) {
  // customProp不会传递给button
  return <button {...buttonProps} />;
}

5. 状态设计原则

5.1 状态归属

tsx
// 状态应该放在最近的公共父组件
function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([]);
  
  return (
    <div>
      <TodoForm onAdd={(todo) => setTodos([...todos, todo])} />
      <TodoList todos={todos} onToggle={(id) => {/* ... */}} />
      <TodoStats todos={todos} />
    </div>
  );
}

5.2 派生状态

tsx
// ❌ 不好: 冗余状态
function UserList({ users }: { users: User[] }) {
  const [filteredUsers, setFilteredUsers] = useState(users);
  const [filter, setFilter] = useState('');
  
  useEffect(() => {
    setFilteredUsers(
      users.filter(u => u.name.includes(filter))
    );
  }, [users, filter]);
  
  // ...
}

// ✅ 好: 派生状态
function UserList({ users }: { users: User[] }) {
  const [filter, setFilter] = useState('');
  
  const filteredUsers = useMemo(
    () => users.filter(u => u.name.includes(filter)),
    [users, filter]
  );
  
  // ...
}

6. 组件文档化

tsx
/**
 * Button组件
 * 
 * 通用按钮组件,支持多种样式和状态
 * 
 * @example
 * ```tsx
 * <Button variant="primary" onClick={handleClick}>
 *   Click Me
 * </Button>
 * ```
 */
interface ButtonProps {
  /** 按钮内容 */
  children: React.ReactNode;
  
  /** 按钮样式变体 */
  variant?: 'primary' | 'secondary' | 'danger';
  
  /** 是否显示加载状态 */
  isLoading?: boolean;
  
  /** 点击事件处理器 */
  onClick?: () => void;
}

export function Button({
  children,
  variant = 'primary',
  isLoading = false,
  onClick
}: ButtonProps) {
  // ...
}

7. 总结

优秀的组件设计应该:

  1. 遵循SOLID原则: 单一职责、开闭、里氏替换、接口隔离、依赖倒置
  2. 应用设计模式: 容器/展示、复合组件、Render Props、HOC、Hooks组合
  3. 合理拆分: 按职责、复用性、数据流、性能需求拆分
  4. 规范Props: 清晰命名、合理解构、正确传递
  5. 优化状态: 状态归属明确、避免冗余、使用派生状态
  6. 完善文档: JSDoc注释、使用示例、类型定义

持续实践这些原则可以构建可维护、可扩展的React应用。