React组件系统

第三章 组件系统

React 的核心思想是组件化开发,将用户界面拆分为独立、可复用的组件,每个组件管理自己的状态和逻辑。本章将深入探讨 React 组件系统的各个方面。

3.1 组件基础概念

3.1.1 组件的定义与分类

组件的本质:

graph TD A[React 组件] --> B[函数组件] A --> C[类组件] B --> D[无状态组件] B --> E[有状态组件 - Hooks] C --> F[有状态组件] C --> G[生命周期方法] H[组件特点] --> I[封装性] H --> J[可复用性] H --> K[组合性] H --> L[单向数据流]

组件分类详解:

// 1. 无状态函数组件
const WelcomeMessage = ({ name, age }) => {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      {age && <p>Age: {age}</p>}
    </div>
  );
};

// 2. 有状态函数组件 (使用 Hooks)
const Counter = () => {
  const [count, setCount] = useState(0);
  const [isIncrementing, setIsIncrementing] = useState(false);

  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
};

// 3. 类组件
class UserProfile extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isEditing: false,
      formData: { ...props.user }
    };
  }

  handleEdit = () => {
    this.setState({ isEditing: true });
  };

  handleSave = () => {
    this.props.onSave(this.state.formData);
    this.setState({ isEditing: false });
  };

  handleChange = (field, value) => {
    this.setState(prevState => ({
      formData: {
        ...prevState.formData,
        [field]: value
      }
    }));
  };

  render() {
    const { user } = this.props;
    const { isEditing, formData } = this.state;

    return (
      <div className="user-profile">
        {isEditing ? (
          <div>
            <input
              value={formData.name}
              onChange={(e) => this.handleChange('name', e.target.value)}
            />
            <input
              value={formData.email}
              onChange={(e) => this.handleChange('email', e.target.value)}
            />
            <button onClick={this.handleSave}>Save</button>
          </div>
        ) : (
          <div>
            <h3>{user.name}</h3>
            <p>{user.email}</p>
            <button onClick={this.handleEdit}>Edit</button>
          </div>
        )}
      </div>
    );
  }
}

// 4. 高阶组件 (HOC)
const withLogging = (WrappedComponent) => {
  return class extends React.Component {
    componentDidMount() {
      console.log(`${WrappedComponent.name} mounted`);
    }

    componentWillUnmount() {
      console.log(`${WrappedComponent.name} unmounted`);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
};

const LoggedCounter = withLogging(Counter);

// 5. 渲染属性组件 (Render Props)
class MouseTracker extends React.Component {
  state = {
    x: 0,
    y: 0
  };

  handleMouseMove = (e) => {
    this.setState({
      x: e.clientX,
      y: e.clientY
    });
  };

  render() {
    return (
      <div onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>
    );
  }
}

// 使用方式
const App = () => {
  return (
    <div>
      <WelcomeMessage name="Alice" age={25} />
      <Counter />
      <UserProfile 
        user={{ name: 'Bob', email: 'bob@example.com' }}
        onSave={(data) => console.log('Saved:', data)}
      />
      <LoggedCounter />
      <MouseTracker 
        render={({ x, y }) => (
          <div>Mouse position: ({x}, {y})</div>
        )} 
      />
    </div>
  );
};

3.1.2 组件的设计原则

单一职责原则:

// ❌ 违反单一职责原则
const UserProfileAndSettings = ({ user }) => {
  const [isEditing, setIsEditing] = useState(false);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState(true);
  const [language, setLanguage] = useState('en');

  return (
    <div>
      {/* 用户信息显示和编辑 */}
      <div className="user-profile">
        {isEditing ? (
          <form>
            <input defaultValue={user.name} />
            <input defaultValue={user.email} />
          </form>
        ) : (
          <div>
            <h3>{user.name}</h3>
            <p>{user.email}</p>
          </div>
        )}
        <button onClick={() => setIsEditing(!isEditing)}>
          {isEditing ? 'Save' : 'Edit'}
        </button>
      </div>

      {/* 设置面板 */}
      <div className="settings">
        <h4>Settings</h4>
        <label>
          Theme:
          <select value={theme} onChange={(e) => setTheme(e.target.value)}>
            <option value="light">Light</option>
            <option value="dark">Dark</option>
          </select>
        </label>
        <label>
          Notifications:
          <input
            type="checkbox"
            checked={notifications}
            onChange={(e) => setNotifications(e.target.checked)}
          />
        </label>
        <label>
          Language:
          <select value={language} onChange={(e) => setLanguage(e.target.value)}>
            <option value="en">English</option>
            <option value="zh">中文</option>
          </select>
        </label>
      </div>
    </div>
  );
};

// ✅ 遵循单一职责原则
const UserProfile = ({ user, onEdit }) => {
  return (
    <div className="user-profile">
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <button onClick={onEdit}>Edit</button>
    </div>
  );
};

const UserEditForm = ({ user, onSave, onCancel }) => {
  const [formData, setFormData] = useState(user);

  const handleSubmit = (e) => {
    e.preventDefault();
    onSave(formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={formData.name}
        onChange={(e) => setFormData({ ...formData, name: e.target.value })}
      />
      <input
        value={formData.email}
        onChange={(e) => setFormData({ ...formData, email: e.target.value })}
      />
      <button type="submit">Save</button>
      <button type="button" onClick={onCancel}>Cancel</button>
    </form>
  );
};

const SettingsPanel = ({ theme, notifications, language, onThemeChange, onNotificationsChange, onLanguageChange }) => {
  return (
    <div className="settings">
      <h4>Settings</h4>
      <label>
        Theme:
        <select value={theme} onChange={(e) => onThemeChange(e.target.value)}>
          <option value="light">Light</option>
          <option value="dark">Dark</option>
        </select>
      </label>
      <label>
        Notifications:
        <input
          type="checkbox"
          checked={notifications}
          onChange={(e) => onNotificationsChange(e.target.checked)}
        />
      </label>
      <label>
        Language:
        <select value={language} onChange={(e) onLanguageChange(e.target.value)}>
          <option value="en">English</option>
          <option value="zh">中文</option>
        </select>
      </label>
    </div>
  );
};

// 组合组件
const UserProfileAndSettings = ({ user }) => {
  const [isEditing, setIsEditing] = useState(false);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState(true);
  const [language, setLanguage] = useState('en');

  const handleEdit = () => setIsEditing(true);
  const handleSave = (userData) => {
    // 保存用户数据
    setIsEditing(false);
  };
  const handleCancel = () => setIsEditing(false);

  return (
    <div>
      {isEditing ? (
        <UserEditForm
          user={user}
          onSave={handleSave}
          onCancel={handleCancel}
        />
      ) : (
        <UserProfile user={user} onEdit={handleEdit} />
      )}
      <SettingsPanel
        theme={theme}
        notifications={notifications}
        language={language}
        onThemeChange={setTheme}
        onNotificationsChange={setNotifications}
        onLanguageChange={setLanguage}
      />
    </div>
  );
};

开闭原则:

// ✅ 可扩展的组件设计
const DataTable = ({ 
  data, 
  columns, 
  onRowClick, 
  rowClassName,
  emptyMessage = 'No data available'
}) => {
  if (!data || data.length === 0) {
    return <div className="empty-message">{emptyMessage}</div>;
  }

  return (
    <table className="data-table">
      <thead>
        <tr>
          {columns.map(column => (
            <th key={column.key}>{column.title}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data.map((row, index) => (
          <tr
            key={row.id || index}
            onClick={() => onRowClick && onRowClick(row)}
            className={rowClassName ? rowClassName(row, index) : ''}
          >
            {columns.map(column => (
              <td key={column.key}>
                {column.render ? column.render(row[column.dataIndex], row) : row[column.dataIndex]}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
};

// 使用示例 - 不同场景的扩展
const UserTable = ({ users, onUserClick }) => {
  const userColumns = [
    {
      key: 'name',
      title: 'Name',
      dataIndex: 'name',
      render: (text, record) => <strong>{text}</strong>
    },
    {
      key: 'email',
      title: 'Email',
      dataIndex: 'email'
    },
    {
      key: 'status',
      title: 'Status',
      dataIndex: 'status',
      render: (status) => (
        <span className={`status-${status}`}>
          {status}
        </span>
      )
    },
    {
      key: 'actions',
      title: 'Actions',
      render: (_, record) => (
        <div>
          <button onClick={(e) => {
            e.stopPropagation();
            console.log('Edit user:', record.id);
          }}>
            Edit
          </button>
          <button onClick={(e) => {
            e.stopPropagation();
            console.log('Delete user:', record.id);
          }}>
            Delete
          </button>
        </div>
      )
    }
  ];

  return (
    <DataTable
      data={users}
      columns={userColumns}
      onRowClick={onUserClick}
      rowClassName={(row) => row.status === 'inactive' ? 'inactive-row' : ''}
    />
  );
};

const ProductTable = ({ products, onProductSelect }) => {
  const productColumns = [
    {
      key: 'image',
      title: 'Image',
      dataIndex: 'image',
      render: (url) => <img src={url} alt="Product" width="50" />
    },
    {
      key: 'name',
      title: 'Product Name',
      dataIndex: 'name'
    },
    {
      key: 'price',
      title: 'Price',
      dataIndex: 'price',
      render: (price) => `$${price.toFixed(2)}`
    },
    {
      key: 'inStock',
      title: 'Availability',
      dataIndex: 'inStock',
      render: (inStock) => inStock ? '✓ In Stock' : '✗ Out of Stock'
    }
  ];

  return (
    <DataTable
      data={products}
      columns={productColumns}
      onRowClick={onProductSelect}
      rowClassName={(row) => !row.inStock ? 'out-of-stock' : ''}
      emptyMessage="No products found"
    />
  );
};

3.2 组件通信

3.2.1 Props 传递与验证

Props 传递模式:

import React from 'react';
import PropTypes from 'prop-types';

// 基础 Props 传递
const Button = ({ 
  text, 
  onClick, 
  variant = 'primary',
  size = 'medium',
  disabled = false,
  icon,
  loading = false,
  children,
  ...restProps 
}) => {
  const baseClasses = 'btn';
  const variantClasses = `btn-${variant}`;
  const sizeClasses = `btn-${size}`;
  const disabledClasses = disabled ? 'btn-disabled' : '';
  const loadingClasses = loading ? 'btn-loading' : '';

  return (
    <button
      className={`${baseClasses} ${variantClasses} ${sizeClasses} ${disabledClasses} ${loadingClasses}`}
      onClick={onClick}
      disabled={disabled || loading}
      {...restProps}
    >
      {loading && <span className="spinner" />}
      {icon && <span className="icon">{icon}</span>}
      {children || text}
    </button>
  );
};

// PropTypes 验证
Button.propTypes = {
  text: PropTypes.string,
  onClick: PropTypes.func,
  variant: PropTypes.oneOf(['primary', 'secondary', 'success', 'warning', 'danger']),
  size: PropTypes.oneOf(['small', 'medium', 'large']),
  disabled: PropTypes.bool,
  icon: PropTypes.node,
  loading: PropTypes.bool,
  children: PropTypes.node
};

// 默认 Props
Button.defaultProps = {
  variant: 'primary',
  size: 'medium',
  disabled: false,
  loading: false
};

// TypeScript Props 定义
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  text?: string;
  onClick?: () => void;
  variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
  size?: 'small' | 'medium' | 'large';
  disabled?: boolean;
  icon?: React.ReactNode;
  loading?: boolean;
}

const Button: React.FC<ButtonProps> = ({
  text,
  onClick,
  variant = 'primary',
  size = 'medium',
  disabled = false,
  icon,
  loading = false,
  children,
  ...restProps
}) => {
  // 组件实现
};

复杂 Props 传递模式:

// 配置对象模式
const DataVisualization = ({ config }) => {
  const {
    type,
    data,
    dimensions,
    colors,
    interactions,
    animations,
    responsive,
    theme
  } = config;

  // 解构简化了 Props 结构
  return (
    <div className="data-viz">
      {/* 基于配置渲染可视化 */}
    </div>
  );
};

// 使用配置模式
const chartConfig = {
  type: 'bar',
  data: [...],
  dimensions: {
    width: 800,
    height: 400,
    margin: { top: 20, right: 20, bottom: 40, left: 40 }
  },
  colors: ['#007bff', '#28a745', '#ffc107', '#dc3545'],
  interactions: {
    hover: true,
    click: true,
    zoom: false
  },
  animations: {
    duration: 300,
    easing: 'ease-out'
  },
  responsive: true,
  theme: 'light'
};

// Render Props 模式
const DataProvider = ({ children, endpoint, method = 'GET' }) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      setError(null);
      
      try {
        const response = await fetch(endpoint, { method });
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [endpoint, method]);

  // 传递数据和方法给子组件
  return children({ data, loading, error, refetch: fetchData });
};

// 使用 Render Props
const UserList = () => {
  return (
    <DataProvider endpoint="/api/users">
      {({ data, loading, error, refetch }) => (
        <div>
          {loading && <div>Loading users...</div>}
          {error && <div>Error: {error}</div>}
          {data && (
            <ul>
              {data.map(user => (
                <li key={user.id}>{user.name}</li>
              ))}
            </ul>
          )}
          <button onClick={refetch}>Refresh</button>
        </div>
      )}
    </DataProvider>
  );
};

3.2.2 Context 深度传递

Context 基础使用:

import React, { createContext, useContext, useState, useEffect } from 'react';

// 1. 创建 Context
const AppContext = createContext();

// 2. Context Provider 组件
const AppProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);

  // 模拟用户认证
  useEffect(() => {
    const savedUser = localStorage.getItem('user');
    if (savedUser) {
      setUser(JSON.parse(savedUser));
    }
  }, []);

  const login = async (credentials) => {
    try {
      // 模拟 API 调用
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials)
      });
      
      const userData = await response.json();
      setUser(userData);
      localStorage.setItem('user', JSON.stringify(userData));
      
      return { success: true };
    } catch (error) {
      return { success: false, error: error.message };
    }
  };

  const logout = () => {
    setUser(null);
    localStorage.removeItem('user');
  };

  const addNotification = (notification) => {
    const id = Date.now();
    setNotifications(prev => [...prev, { ...notification, id }]);
    
    // 自动移除通知
    setTimeout(() => {
      setNotifications(prev => prev.filter(n => n.id !== id));
    }, 5000);
  };

  const value = {
    // 状态
    user,
    theme,
    notifications,
    
    // 方法
    setUser,
    setTheme,
    login,
    logout,
    addNotification
  };

  return (
    <AppContext.Provider value={value}>
      {children}
    </AppContext.Provider>
  );
};

// 3. 自定义 Hook
const useAppContext = () => {
  const context = useContext(AppContext);
  
  if (!context) {
    throw new Error('useAppContext must be used within AppProvider');
  }
  
  return context;
};

// 4. 专用 Context Hooks
const useAuth = () => {
  const { user, login, logout } = useAppContext();
  return { user, login, logout, isAuthenticated: !!user };
};

const useTheme = () => {
  const { theme, setTheme } = useAppContext();
  return { theme, setTheme, isDark: theme === 'dark' };
};

const useNotifications = () => {
  const { notifications, addNotification } = useAppContext();
  return { notifications, addNotification };
};

// 5. 使用示例
const Header = () => {
  const { user, logout } = useAuth();
  const { theme, setTheme } = useTheme();

  return (
    <header className="header">
      <h1>My App</h1>
      <nav>
        <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
          {theme === 'light' ? '🌙' : '☀️'}
        </button>
        {user ? (
          <div>
            <span>Welcome, {user.name}!</span>
            <button onClick={logout}>Logout</button>
          </div>
        ) : (
          <LoginModal />
        )}
      </nav>
    </header>
  );
};

const LoginModal = () => {
  const { login } = useAuth();
  const [credentials, setCredentials] = useState({ username: '', password: '' });
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError('');
    
    const result = await login(credentials);
    
    if (!result.success) {
      setError(result.error);
    }
    
    setLoading(false);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="Username"
        value={credentials.username}
        onChange={(e) => setCredentials({ ...credentials, username: e.target.value })}
      />
      <input
        type="password"
        placeholder="Password"
        value={credentials.password}
        onChange={(e) => setCredentials({ ...credentials, password: e.target.value })}
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Logging in...' : 'Login'}
      </button>
      {error && <div className="error">{error}</div>}
    </form>
  );
};

const NotificationSystem = () => {
  const { notifications } = useNotifications();

  return (
    <div className="notification-container">
      {notifications.map(notification => (
        <div key={notification.id} className={`notification ${notification.type}`}>
          {notification.message}
        </div>
      ))}
    </div>
  );
};

// 应用根组件
const App = () => {
  return (
    <AppProvider>
      <div className="app">
        <Header />
        <main>
          <Dashboard />
        </main>
        <NotificationSystem />
      </div>
    </AppProvider>
  );
};

分层 Context 设计:

// 分层 Context 示例
const AuthContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();
const SettingsContext = createContext();

// 专门的 Provider
const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [permissions, setPermissions] = useState([]);

  const login = async (credentials) => {
    // 登录逻辑
  };

  const logout = () => {
    setUser(null);
    setPermissions([]);
  };

  const hasPermission = (permission) => {
    return permissions.includes(permission);
  };

  return (
    <AuthContext.Provider value={{
      user,
      permissions,
      login,
      logout,
      hasPermission,
      isAuthenticated: !!user
    }}>
      {children}
    </AuthContext.Provider>
  );
};

const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');
  const [customColors, setCustomColors] = useState({});

  return (
    <ThemeContext.Provider value={{
      theme,
      setTheme,
      customColors,
      setCustomColors,
      isDark: theme === 'dark'
    }}>
      {children}
    </ThemeContext.Provider>
  );
};

// 组合 Provider
const AppProviders = ({ children }) => {
  return (
    <AuthProvider>
      <ThemeProvider>
        <NotificationProvider>
          <SettingsProvider>
            {children}
          </SettingsProvider>
        </NotificationProvider>
      </ThemeProvider>
    </AuthProvider>
  );
};

// 使用分层 Context
const AdminPanel = () => {
  const { hasPermission } = useContext(AuthContext);
  const { theme } = useContext(ThemeContext);

  if (!hasPermission('admin_access')) {
    return <div>Access Denied</div>;
  }

  return (
    <div className={`admin-panel theme-${theme}`}>
      <h2>Admin Panel</h2>
      <UserManagement />
      <SystemSettings />
    </div>
  );
};

3.2.3 状态提升与回调

状态提升模式:

// 父组件管理共享状态
const TemperatureConverter = () => {
  const [celsius, setCelsius] = useState('');
  const [fahrenheit, setFahrenheit] = useState('');

  // 摄氏度变化处理
  const handleCelsiusChange = (value) => {
    setCelsius(value);
    if (value === '') {
      setFahrenheit('');
    } else {
      const f = (parseFloat(value) * 9/5) + 32;
      setFahrenheit(f.toFixed(2));
    }
  };

  // 华氏度变化处理
  const handleFahrenheitChange = (value) => {
    setFahrenheit(value);
    if (value === '') {
      setCelsius('');
    } else {
      const c = (parseFloat(value) - 32) * 5/9;
      setCelsius(c.toFixed(2));
    }
  };

  return (
    <div className="temperature-converter">
      <h2>Temperature Converter</h2>
      <div className="converter-container">
        <CelsiusInput 
          value={celsius} 
          onChange={handleCelsiusChange} 
        />
        <div className="equals">=</div>
        <FahrenheitInput 
          value={fahrenheit} 
          onChange={handleFahrenheitChange} 
        />
      </div>
    </div>
  );
};

const CelsiusInput = ({ value, onChange }) => {
  return (
    <div className="input-group">
      <label>Celsius:</label>
      <input
        type="number"
        value={value}
        onChange={(e) => onChange(e.target.value)}
        placeholder="Enter Celsius"
      />
      <span>°C</span>
    </div>
  );
};

const FahrenheitInput = ({ value, onChange }) => {
  return (
    <div className="input-group">
      <label>Fahrenheit:</label>
      <input
        type="number"
        value={value}
        onChange={(e) => onChange(e.target.value)}
        placeholder="Enter Fahrenheit"
      />
      <span>°F</span>
    </div>
  );
};

复杂状态的回调管理:

// 多个组件共享复杂状态
const ShoppingApp = () => {
  const [cart, setCart] = useState([]);
  const [products, setProducts] = useState([]);
  const [filters, setFilters] = useState({
    category: 'all',
    priceRange: [0, 1000],
    inStock: true
  });
  const [isLoading, setIsLoading] = useState(false);

  // 购物车操作
  const addToCart = (product) => {
    setCart(prevCart => {
      const existingItem = prevCart.find(item => item.id === product.id);
      if (existingItem) {
        return prevCart.map(item =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      }
      return [...prevCart, { ...product, quantity: 1 }];
    });
  };

  const removeFromCart = (productId) => {
    setCart(prevCart => prevCart.filter(item => item.id !== productId));
  };

  const updateQuantity = (productId, quantity) => {
    if (quantity <= 0) {
      removeFromCart(productId);
    } else {
      setCart(prevCart =>
        prevCart.map(item =>
          item.id === productId ? { ...item, quantity } : item
        )
      );
    }
  };

  const clearCart = () => {
    setCart([]);
  };

  // 过滤操作
  const updateFilters = (newFilters) => {
    setFilters(prev => ({ ...prev, ...newFilters }));
  };

  const filteredProducts = products.filter(product => {
    if (filters.category !== 'all' && product.category !== filters.category) {
      return false;
    }
    if (product.price < filters.priceRange[0] || product.price > filters.priceRange[1]) {
      return false;
    }
    if (filters.inStock && !product.inStock) {
      return false;
    }
    return true;
  });

  const cartTotal = cart.reduce((total, item) => total + (item.price * item.quantity), 0);
  const cartItemCount = cart.reduce((total, item) => total + item.quantity, 0);

  return (
    <div className="shopping-app">
      <header>
        <h1>Shopping Store</h1>
        <CartBadge count={cartItemCount} total={cartTotal} />
      </header>

      <main>
        <aside>
          <FilterPanel 
            filters={filters} 
            onFilterChange={updateFilters} 
            categories={[...new Set(products.map(p => p.category))]}
          />
        </aside>

        <section>
          <ProductGrid 
            products={filteredProducts}
            onAddToCart={addToCart}
            isLoading={isLoading}
          />
        </section>
      </main>

      <CartWidget 
        cart={cart}
        onRemoveItem={removeFromCart}
        onUpdateQuantity={updateQuantity}
        onClearCart={clearCart}
      />
    </div>
  );
};

const CartBadge = ({ count, total }) => {
  return (
    <div className="cart-badge">
      <span className="count">🛒 {count}</span>
      <span className="total">${total.toFixed(2)}</span>
    </div>
  );
};

const FilterPanel = ({ filters, onFilterChange, categories }) => {
  const [priceRange, setPriceRange] = useState(filters.priceRange);

  const handlePriceRangeChange = (type, value) => {
    const newRange = type === 'min' 
      ? [value, priceRange[1]]
      : [priceRange[0], value];
    setPriceRange(newRange);
    onFilterChange({ priceRange: newRange });
  };

  return (
    <div className="filter-panel">
      <h3>Filters</h3>
      
      <div className="filter-group">
        <label>Category:</label>
        <select 
          value={filters.category} 
          onChange={(e) => onFilterChange({ category: e.target.value })}
        >
          <option value="all">All Categories</option>
          {categories.map(category => (
            <option key={category} value={category}>
              {category}
            </option>
          ))}
        </select>
      </div>

      <div className="filter-group">
        <label>Price Range:</label>
        <div>
          <input
            type="number"
            placeholder="Min"
            value={priceRange[0]}
            onChange={(e) => handlePriceRangeChange('min', parseFloat(e.target.value) || 0)}
          />
          <span>-</span>
          <input
            type="number"
            placeholder="Max"
            value={priceRange[1]}
            onChange={(e) => handlePriceRangeChange('max', parseFloat(e.target.value) || 1000)}
          />
        </div>
      </div>

      <div className="filter-group">
        <label>
          <input
            type="checkbox"
            checked={filters.inStock}
            onChange={(e) => onFilterChange({ inStock: e.target.checked })}
          />
          In Stock Only
        </label>
      </div>
    </div>
  );
};

const ProductGrid = ({ products, onAddToCart, isLoading }) => {
  if (isLoading) {
    return <div>Loading products...</div>;
  }

  if (products.length === 0) {
    return <div>No products found matching your filters.</div>;
  }

  return (
    <div className="product-grid">
      {products.map(product => (
        <ProductCard 
          key={product.id}
          product={product}
          onAddToCart={onAddToCart}
        />
      ))}
    </div>
  );
};

const ProductCard = ({ product, onAddToCart }) => {
  return (
    <div className="product-card">
      <img src={product.image} alt={product.name} />
      <h4>{product.name}</h4>
      <p className="price">${product.price.toFixed(2)}</p>
      <p className="category">{product.category}</p>
      {!product.inStock && <span className="out-of-stock">Out of Stock</span>}
      <button 
        onClick={() => onAddToCart(product)}
        disabled={!product.inStock}
      >
        {product.inStock ? 'Add to Cart' : 'Out of Stock'}
      </button>
    </div>
  );
};

const CartWidget = ({ cart, onRemoveItem, onUpdateQuantity, onClearCart }) => {
  if (cart.length === 0) {
    return (
      <div className="cart-widget empty">
        <h3>Your Cart</h3>
        <p>Your cart is empty</p>
      </div>
    );
  }

  const total = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);

  return (
    <div className="cart-widget">
      <div className="cart-header">
        <h3>Your Cart</h3>
        <button onClick={onClearCart} className="clear-cart">
          Clear All
        </button>
      </div>

      <div className="cart-items">
        {cart.map(item => (
          <CartItem
            key={item.id}
            item={item}
            onRemove={onRemoveItem}
            onUpdateQuantity={onUpdateQuantity}
          />
        ))}
      </div>

      <div className="cart-footer">
        <div className="total">
          <strong>Total: ${total.toFixed(2)}</strong>
        </div>
        <button className="checkout-btn">Checkout</button>
      </div>
    </div>
  );
};

const CartItem = ({ item, onRemove, onUpdateQuantity }) => {
  const handleQuantityChange = (newQuantity) => {
    onUpdateQuantity(item.id, newQuantity);
  };

  return (
    <div className="cart-item">
      <img src={item.image} alt={item.name} className="item-image" />
      <div className="item-details">
        <h4>{item.name}</h4>
        <p>${item.price.toFixed(2)}</p>
      </div>
      <div className="item-quantity">
        <button 
          onClick={() => handleQuantityChange(item.quantity - 1)}
        >
          -
        </button>
        <span>{item.quantity}</span>
        <button 
          onClick={() => handleQuantityChange(item.quantity + 1)}
        >
          +
        </button>
      </div>
      <div className="item-total">
        ${(item.price * item.quantity).toFixed(2)}
      </div>
      <button 
        onClick={() => onRemove(item.id)}
        className="remove-item"
      >
        ×
      </button>
    </div>
  );
};

3.3 组件生命周期

3.3.1 类组件生命周期

生命周期方法详解:

import React, { Component } from 'react';

class UserProfile extends Component {
  constructor(props) {
    super(props);
    
    // 1. 初始化状态
    this.state = {
      user: null,
      loading: true,
      error: null,
      editMode: false,
      formData: {}
    };

    // 绑定方法
    this.handleEdit = this.handleEdit.bind(this);
    this.handleSave = this.handleSave.bind(this);
    this.handleCancel = this.handleCancel.bind(this);
  }

  // 2. 组件挂载前 - getDerivedStateFromProps (静态方法)
  static getDerivedStateFromProps(nextProps, prevState) {
    // 根据 props 更新 state
    if (nextProps.userId !== prevState.userId) {
      return {
        userId: nextProps.userId,
        loading: true,
        user: null,
        error: null
      };
    }
    return null; // 不更新 state
  }

  // 3. 挂载前 - render 之前的最后一次更新 state 的机会
  // 在 React 16.3 后已不推荐使用,被 getDerivedStateFromProps 替代
  
  // 4. 挂载后 - 组件首次渲染后执行
  componentDidMount() {
    console.log('UserProfile mounted');
    
    // 适合的操作:
    // - 发起 API 请求
    // - 设置定时器
    // - 添加事件监听器
    // - 操作 DOM
    
    this.fetchUserData();
    this.setupWindowResizeListener();
  }

  // 5. 更新前 - getSnapshotBeforeUpdate
  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 在更新前获取 DOM 信息,返回值会传递给 componentDidUpdate
    if (prevState.editMode !== this.state.editMode) {
      return { wasEditing: prevState.editMode };
    }
    return null;
  }

  // 6. 更新后 - componentDidUpdate
  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log('UserProfile updated');
    
    // 适合的操作:
    // - 发起新的 API 请求(当 props 变化时)
    // - 操作 DOM(基于新的 state/props)
    // - 清理和重新设置定时器
    
    if (prevProps.userId !== this.props.userId) {
      this.fetchUserData();
    }

    if (snapshot && snapshot.wasEditing !== this.state.editMode) {
      console.log('Edit mode changed from', snapshot.wasEditing, 'to', this.state.editMode);
    }

    // 如果编辑模式开启,自动聚焦到第一个输入框
    if (!prevState.editMode && this.state.editMode) {
      this.focusFirstInput();
    }
  }

  // 7. 卸载前 - 组件销毁前执行
  componentWillUnmount() {
    console.log('UserProfile will unmount');
    
    // 必要的清理操作:
    // - 清除定时器
    // - 移除事件监听器
    // - 取消网络请求
    // - 清理订阅
    
    this.cleanup();
  }

  // 8. 错误处理 - getDerivedStateFromError
  static getDerivedStateFromError(error) {
    // 更新 state 以显示错误 UI
    return { hasError: true, error };
  }

  // 9. 错误处理 - componentDidCatch
  componentDidCatch(error, errorInfo) {
    // 记录错误信息
    console.error('Error caught in UserProfile:', error, errorInfo);
    
    // 可以发送错误报告到监控服务
    this.logErrorToService(error, errorInfo);
  }

  // 自定义方法
  fetchUserData = async () => {
    const { userId } = this.props;
    
    try {
      const response = await fetch(`/api/users/${userId}`);
      const userData = await response.json();
      
      this.setState({
        user: userData,
        loading: false,
        error: null,
        formData: userData
      });
    } catch (error) {
      this.setState({
        user: null,
        loading: false,
        error: error.message
      });
    }
  };

  setupWindowResizeListener = () => {
    this.handleResize = () => {
      // 处理窗口大小变化
      console.log('Window resized');
    };
    window.addEventListener('resize', this.handleResize);
  };

  focusFirstInput = () => {
    if (this.nameInputRef) {
      this.nameInputRef.focus();
    }
  };

  cleanup = () => {
    if (this.handleResize) {
      window.removeEventListener('resize', this.handleResize);
    }
    
    // 取消正在进行的请求
    if (this.abortController) {
      this.abortController.abort();
    }
  };

  handleEdit() {
    this.setState({ editMode: true });
  }

  handleSave() {
    const { formData } = this.state;
    const { onSave } = this.props;

    // 保存数据
    this.setState({ loading: true });
    
    fetch(`/api/users/${formData.id}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(formData)
    })
    .then(response => response.json())
    .then(updatedUser => {
      this.setState({
        user: updatedUser,
        formData: updatedUser,
        editMode: false,
        loading: false
      });
      
      if (onSave) {
        onSave(updatedUser);
      }
    })
    .catch(error => {
      this.setState({
        error: error.message,
        loading: false
      });
    });
  }

  handleCancel() {
    const { user } = this.state;
    this.setState({
      editMode: false,
      formData: user || {}
    });
  }

  handleInputChange = (field, value) => {
    this.setState(prevState => ({
      formData: {
        ...prevState.formData,
        [field]: value
      }
    }));
  };

  render() {
    const { user, loading, error, editMode, formData } = this.state;

    if (loading) {
      return <div className="loading">Loading user profile...</div>;
    }

    if (error) {
      return <div className="error">Error: {error}</div>;
    }

    if (!user) {
      return <div className="not-found">User not found</div>;
    }

    return (
      <div className="user-profile">
        {editMode ? (
          <div className="edit-form">
            <h2>Edit Profile</h2>
            
            <div className="form-group">
              <label>Name:</label>
              <input
                ref={input => this.nameInputRef = input}
                type="text"
                value={formData.name || ''}
                onChange={(e) => this.handleInputChange('name', e.target.value)}
              />
            </div>

            <div className="form-group">
              <label>Email:</label>
              <input
                type="email"
                value={formData.email || ''}
                onChange={(e) => this.handleInputChange('email', e.target.value)}
              />
            </div>

            <div className="form-group">
              <label>Phone:</label>
              <input
                type="tel"
                value={formData.phone || ''}
                onChange={(e) => this.handleInputChange('phone', e.target.value)}
              />
            </div>

            <div className="form-actions">
              <button onClick={this.handleSave} disabled={loading}>
                {loading ? 'Saving...' : 'Save'}
              </button>
              <button onClick={this.handleCancel} disabled={loading}>
                Cancel
              </button>
            </div>
          </div>
        ) : (
          <div className="profile-view">
            <div className="profile-header">
              <img src={user.avatar} alt={user.name} className="avatar" />
              <h2>{user.name}</h2>
              <button onClick={this.handleEdit} className="edit-btn">
                Edit Profile
              </button>
            </div>

            <div className="profile-info">
              <div className="info-item">
                <label>Email:</label>
                <span>{user.email}</span>
              </div>
              
              <div className="info-item">
                <label>Phone:</label>
                <span>{user.phone || 'Not provided'}</span>
              </div>

              <div className="info-item">
                <label>Department:</label>
                <span>{user.department}</span>
              </div>

              <div className="info-item">
                <label>Joined:</label>
                <span>{new Date(user.joinedAt).toLocaleDateString()}</span>
              </div>
            </div>
          </div>
        )}
      </div>
    );
  }
}

// 使用示例
const App = () => {
  const [currentUserId, setCurrentUserId] = useState(1);

  const handleUserSave = (updatedUser) => {
    console.log('User saved:', updatedUser);
  };

  return (
    <div>
      <div className="user-selector">
        <button onClick={() => setCurrentUserId(1)}>User 1</button>
        <button onClick={() => setCurrentUserId(2)}>User 2</button>
        <button onClick={() => setCurrentUserId(3)}>User 3</button>
      </div>
      
      <UserProfile 
        userId={currentUserId}
        onSave={handleUserSave}
      />
    </div>
  );
};

3.3.2 Hooks 替代生命周期

Hooks 生命周期对应关系:

import React, { useState, useEffect, useLayoutEffect, useRef, useCallback, useMemo } from 'react';

// useEffect 对应 componentDidMount, componentDidUpdate, componentWillUnmount
const UserProfile = ({ userId, onSave }) => {
  // State 对应 constructor 和 this.state
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [editMode, setEditMode] = useState(false);
  const [formData, setFormData] = useState({});

  // Ref 对应实例变量
  const nameInputRef = useRef(null);
  const abortControllerRef = useRef(null);

  // useEffect 对应 componentDidMount
  useEffect(() => {
    console.log('UserProfile mounted - equivalent to componentDidMount');
    
    // 组件挂载时执行的操作
    fetchUserData();
    setupWindowResizeListener();

    // useEffect 的返回函数对应 componentWillUnmount
    return () => {
      console.log('UserProfile unmounting - equivalent to componentWillUnmount');
      cleanup();
    };
  }, []); // 空依赖数组确保只在挂载时执行一次

  // useEffect 对应 componentDidUpdate (特定的 props 变化)
  useEffect(() => {
    console.log('UserId changed - equivalent to componentDidUpdate for userId');
    
    if (userId) {
      fetchUserData();
    }
  }, [userId]); // 依赖 userId,当 userId 变化时执行

  // useEffect 对应 componentDidUpdate (特定的 state 变化)
  useEffect(() => {
    if (editMode && nameInputRef.current) {
      console.log('EditMode enabled - focusing input');
      nameInputRef.current.focus();
    }
  }, [editMode]); // 依赖 editMode

  // useLayoutEffect 对应 getSnapshotBeforeUpdate + componentDidUpdate 的同步操作
  useLayoutEffect(() => {
    console.log('Layout effect - synchronous DOM updates');
    // 在 DOM 更新后同步执行,类似 getSnapshotBeforeUpdate 的后续操作
  });

  // useCallback 对应绑定方法,避免重新创建
  const handleEdit = useCallback(() => {
    setEditMode(true);
  }, []);

  const handleSave = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);

      const response = await fetch(`/api/users/${formData.id}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData)
      });

      if (!response.ok) {
        throw new Error('Failed to save user');
      }

      const updatedUser = await response.json();
      
      setUser(updatedUser);
      setFormData(updatedUser);
      setEditMode(false);
      
      if (onSave) {
        onSave(updatedUser);
      }
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, [formData, onSave]);

  const handleCancel = useCallback(() => {
    setEditMode(false);
    setFormData(user || {});
  }, [user]);

  const handleInputChange = useCallback((field, value) => {
    setFormData(prev => ({
      ...prev,
      [field]: value
    }));
  }, []);

  // useMemo 对应计算属性,类似 getDerivedStateFromProps
  const derivedData = useMemo(() => {
    if (!user) return null;
    
    return {
      fullName: `${user.firstName} ${user.lastName}`,
      displayName: user.nickname || `${user.firstName} ${user.lastName}`,
      isActive: user.status === 'active',
      memberSince: new Date(user.joinedAt).toLocaleDateString()
    };
  }, [user]);

  // 模拟数据获取
  const fetchUserData = useCallback(async () => {
    if (!userId) return;

    setLoading(true);
    setError(null);

    // 取消之前的请求
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }

    abortControllerRef.current = new AbortController();

    try {
      const response = await fetch(`/api/users/${userId}`, {
        signal: abortControllerRef.current.signal
      });

      if (!response.ok) {
        throw new Error('Failed to fetch user');
      }

      const userData = await response.json();
      setUser(userData);
      setFormData(userData);
    } catch (err) {
      if (err.name !== 'AbortError') {
        setError(err.message);
        setUser(null);
      }
    } finally {
      setLoading(false);
    }
  }, [userId]);

  // 设置事件监听器
  const setupWindowResizeListener = useCallback(() => {
    const handleResize = () => {
      console.log('Window resized');
    };
    
    window.addEventListener('resize', handleResize);
    
    // 返回清理函数
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  // 清理函数
  const cleanup = useCallback(() => {
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }
  }, []);

  // 渲染逻辑
  if (loading) {
    return <div className="loading">Loading user profile...</div>;
  }

  if (error) {
    return <div className="error">Error: {error}</div>;
  }

  if (!user) {
    return <div className="not-found">User not found</div>;
  }

  return (
    <div className="user-profile">
      {editMode ? (
        <div className="edit-form">
          <h2>Edit Profile</h2>
          
          <div className="form-group">
            <label>Name:</label>
            <input
              ref={nameInputRef}
              type="text"
              value={formData.name || ''}
              onChange={(e) => handleInputChange('name', e.target.value)}
            />
          </div>

          <div className="form-group">
            <label>Email:</label>
            <input
              type="email"
              value={formData.email || ''}
              onChange={(e) => handleInputChange('email', e.target.value)}
            />
          </div>

          <div className="form-group">
            <label>Phone:</label>
            <input
              type="tel"
              value={formData.phone || ''}
              onChange={(e) => handleInputChange('phone', e.target.value)}
            />
          </div>

          <div className="form-actions">
            <button onClick={handleSave} disabled={loading}>
              {loading ? 'Saving...' : 'Save'}
            </button>
            <button onClick={handleCancel} disabled={loading}>
              Cancel
            </button>
          </div>
        </div>
      ) : (
        <div className="profile-view">
          <div className="profile-header">
            <img src={user.avatar} alt={user.name} className="avatar" />
            <h2>{derivedData?.displayName || user.name}</h2>
            <button onClick={handleEdit} className="edit-btn">
              Edit Profile
            </button>
          </div>

          <div className="profile-info">
            <div className="info-item">
              <label>Email:</label>
              <span>{user.email}</span>
            </div>
            
            <div className="info-item">
              <label>Phone:</label>
              <span>{user.phone || 'Not provided'}</span>
            </div>

            <div className="info-item">
              <label>Status:</label>
              <span className={derivedData?.isActive ? 'active' : 'inactive'}>
                {user.status}
              </span>
            </div>

            <div className="info-item">
              <label>Member Since:</label>
              <span>{derivedData?.memberSince}</span>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

// 错误边界组件 (类组件专用功能)
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null, errorInfo: null };
  }

  static getDerivedStateFromError(error) {
    // 对应 getDerivedStateFromError
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 对应 componentDidCatch
    this.setState({
      error: error,
      errorInfo: errorInfo
    });

    // 记录错误到监控服务
    console.error('Error caught by boundary:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-boundary">
          <h2>Something went wrong</h2>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            {this.state.error && this.state.error.toString()}
            <br />
            {this.state.errorInfo.componentStack}
          </details>
          <button onClick={() => this.setState({ hasError: false })}>
            Try again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// 使用示例
const App = () => {
  const [currentUserId, setCurrentUserId] = useState(1);

  const handleUserSave = useCallback((updatedUser) => {
    console.log('User saved:', updatedUser);
  }, []);

  return (
    <ErrorBoundary>
      <div>
        <div className="user-selector">
          <button onClick={() => setCurrentUserId(1)}>User 1</button>
          <button onClick={() => setCurrentUserId(2)}>User 2</button>
          <button onClick={() => setCurrentUserId(999)}>Invalid User</button>
        </div>
        
        <UserProfile 
          userId={currentUserId}
          onSave={handleUserSave}
        />
      </div>
    </ErrorBoundary>
  );
};

通过本章的组件系统详解,你已经全面掌握了 React 组件的设计原则、通信方式、生命周期管理以及 Hooks 的使用技巧。这些知识将帮助你构建结构清晰、可维护性强的 React 应用。

posted @ 2025-11-29 18:15  seven3306  阅读(1)  评论(0)    收藏  举报