Skip to content

React 组件间通信

概述

在 React 应用中,组件间的数据传递和通信是构建复杂界面的关键。本章将学习各种组件间通信的方式,包括父子通信、兄弟组件通信、跨层级通信等,以及相应的最佳实践。

📤 父子组件通信

父组件向子组件传递数据

jsx
// 子组件:用户卡片
function UserCard({ user, onEdit, onDelete, showActions = true }) {
  return (
    <div style={{
      border: '1px solid #ddd',
      borderRadius: '8px',
      padding: '16px',
      margin: '8px',
      backgroundColor: '#f9f9f9'
    }}>
      <div style={{ display: 'flex', alignItems: 'center', marginBottom: '12px' }}>
        <img 
          src={user.avatar || '/default-avatar.png'} 
          alt={user.name}
          style={{ width: '50px', height: '50px', borderRadius: '50%', marginRight: '12px' }}
        />
        <div>
          <h3 style={{ margin: 0 }}>{user.name}</h3>
          <p style={{ margin: 0, color: '#666' }}>{user.email}</p>
        </div>
      </div>
      
      <div style={{ marginBottom: '12px' }}>
        <p><strong>职位:</strong> {user.position}</p>
        <p><strong>部门:</strong> {user.department}</p>
        <p><strong>状态:</strong> 
          <span style={{ 
            color: user.isActive ? 'green' : 'red',
            fontWeight: 'bold'
          }}>
            {user.isActive ? '在线' : '离线'}
          </span>
        </p>
      </div>
      
      {showActions && (
        <div style={{ display: 'flex', gap: '8px' }}>
          <button 
            onClick={() => onEdit(user.id)}
            style={{ backgroundColor: '#007bff', color: 'white', border: 'none', padding: '8px 16px', borderRadius: '4px' }}
          >
            编辑
          </button>
          <button 
            onClick={() => onDelete(user.id)}
            style={{ backgroundColor: '#dc3545', color: 'white', border: 'none', padding: '8px 16px', borderRadius: '4px' }}
          >
            删除
          </button>
        </div>
      )}
    </div>
  );
}

// 父组件:用户列表
function UserList() {
  const [users, setUsers] = React.useState([
    {
      id: 1,
      name: '张三',
      email: 'zhangsan@example.com',
      position: '前端开发工程师',
      department: '技术部',
      isActive: true,
      avatar: '/avatars/zhangsan.jpg'
    },
    {
      id: 2,
      name: '李四',
      email: 'lisi@example.com',
      position: '后端开发工程师',
      department: '技术部',
      isActive: false,
      avatar: '/avatars/lisi.jpg'
    }
  ]);
  
  const [filter, setFilter] = React.useState('all');
  
  const handleEdit = (userId) => {
    console.log('编辑用户:', userId);
    // 实现编辑逻辑
  };
  
  const handleDelete = (userId) => {
    if (window.confirm('确定要删除这个用户吗?')) {
      setUsers(users.filter(user => user.id !== userId));
    }
  };
  
  const filteredUsers = users.filter(user => {
    if (filter === 'active') return user.isActive;
    if (filter === 'inactive') return !user.isActive;
    return true;
  });
  
  return (
    <div>
      <h2>用户管理</h2>
      
      {/* 过滤器 */}
      <div style={{ marginBottom: '20px' }}>
        <label>过滤: </label>
        <select value={filter} onChange={(e) => setFilter(e.target.value)}>
          <option value="all">全部</option>
          <option value="active">在线</option>
          <option value="inactive">离线</option>
        </select>
      </div>
      
      {/* 用户列表 */}
      <div>
        {filteredUsers.map(user => (
          <UserCard
            key={user.id}
            user={user}
            onEdit={handleEdit}
            onDelete={handleDelete}
            showActions={true}
          />
        ))}
      </div>
      
      {filteredUsers.length === 0 && (
        <div style={{ textAlign: 'center', padding: '40px', color: '#666' }}>
          没有找到符合条件的用户
        </div>
      )}
    </div>
  );
}

子组件向父组件传递数据

jsx
// 子组件:表单
function UserForm({ initialUser = null, onSubmit, onCancel }) {
  const [formData, setFormData] = React.useState({
    name: initialUser?.name || '',
    email: initialUser?.email || '',
    position: initialUser?.position || '',
    department: initialUser?.department || ''
  });
  
  const [errors, setErrors] = React.useState({});
  
  const validateForm = () => {
    const newErrors = {};
    
    if (!formData.name.trim()) {
      newErrors.name = '姓名不能为空';
    }
    
    if (!formData.email.trim()) {
      newErrors.email = '邮箱不能为空';
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = '邮箱格式不正确';
    }
    
    if (!formData.position.trim()) {
      newErrors.position = '职位不能为空';
    }
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
    
    // 清除对应字段的错误
    if (errors[name]) {
      setErrors(prev => ({ ...prev, [name]: '' }));
    }
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    if (validateForm()) {
      // 向父组件传递数据
      onSubmit({
        ...formData,
        id: initialUser?.id || Date.now()
      });
    }
  };
  
  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: '400px', margin: '20px 0' }}>
      <h3>{initialUser ? '编辑用户' : '添加用户'}</h3>
      
      <div style={{ marginBottom: '15px' }}>
        <label>姓名:</label>
        <input
          type="text"
          name="name"
          value={formData.name}
          onChange={handleChange}
          style={{ 
            width: '100%', 
            padding: '8px', 
            borderColor: errors.name ? 'red' : '#ccc'
          }}
        />
        {errors.name && <div style={{ color: 'red', fontSize: '12px' }}>{errors.name}</div>}
      </div>
      
      <div style={{ marginBottom: '15px' }}>
        <label>邮箱:</label>
        <input
          type="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
          style={{ 
            width: '100%', 
            padding: '8px',
            borderColor: errors.email ? 'red' : '#ccc'
          }}
        />
        {errors.email && <div style={{ color: 'red', fontSize: '12px' }}>{errors.email}</div>}
      </div>
      
      <div style={{ marginBottom: '15px' }}>
        <label>职位:</label>
        <input
          type="text"
          name="position"
          value={formData.position}
          onChange={handleChange}
          style={{ 
            width: '100%', 
            padding: '8px',
            borderColor: errors.position ? 'red' : '#ccc'
          }}
        />
        {errors.position && <div style={{ color: 'red', fontSize: '12px' }}>{errors.position}</div>}
      </div>
      
      <div style={{ marginBottom: '15px' }}>
        <label>部门:</label>
        <select
          name="department"
          value={formData.department}
          onChange={handleChange}
          style={{ width: '100%', padding: '8px' }}
        >
          <option value="">请选择部门</option>
          <option value="技术部">技术部</option>
          <option value="产品部">产品部</option>
          <option value="设计部">设计部</option>
          <option value="市场部">市场部</option>
        </select>
      </div>
      
      <div style={{ display: 'flex', gap: '10px' }}>
        <button type="submit" style={{ flex: 1, padding: '10px', backgroundColor: '#007bff', color: 'white', border: 'none' }}>
          {initialUser ? '更新' : '添加'}
        </button>
        <button type="button" onClick={onCancel} style={{ flex: 1, padding: '10px', backgroundColor: '#6c757d', color: 'white', border: 'none' }}>
          取消
        </button>
      </div>
    </form>
  );
}

// 父组件:用户管理
function UserManager() {
  const [users, setUsers] = React.useState([]);
  const [showForm, setShowForm] = React.useState(false);
  const [editingUser, setEditingUser] = React.useState(null);
  
  const handleAddUser = (userData) => {
    setUsers(prev => [...prev, { ...userData, isActive: true }]);
    setShowForm(false);
  };
  
  const handleEditUser = (userData) => {
    setUsers(prev => prev.map(user => 
      user.id === userData.id ? { ...user, ...userData } : user
    ));
    setEditingUser(null);
    setShowForm(false);
  };
  
  const handleDeleteUser = (userId) => {
    setUsers(prev => prev.filter(user => user.id !== userId));
  };
  
  const startEdit = (userId) => {
    const user = users.find(u => u.id === userId);
    setEditingUser(user);
    setShowForm(true);
  };
  
  const cancelForm = () => {
    setShowForm(false);
    setEditingUser(null);
  };
  
  return (
    <div>
      <h2>用户管理系统</h2>
      
      <button 
        onClick={() => setShowForm(true)}
        disabled={showForm}
        style={{ 
          padding: '10px 20px', 
          backgroundColor: '#28a745', 
          color: 'white', 
          border: 'none', 
          borderRadius: '4px',
          marginBottom: '20px'
        }}
      >
        添加用户
      </button>
      
      {showForm && (
        <UserForm
          initialUser={editingUser}
          onSubmit={editingUser ? handleEditUser : handleAddUser}
          onCancel={cancelForm}
        />
      )}
      
      <div>
        {users.map(user => (
          <UserCard
            key={user.id}
            user={user}
            onEdit={startEdit}
            onDelete={handleDeleteUser}
          />
        ))}
      </div>
    </div>
  );
}

🔄 兄弟组件通信

通过共同父组件通信

jsx
// 产品组件
function ProductList({ products, onSelectProduct }) {
  return (
    <div style={{ width: '300px', border: '1px solid #ddd', padding: '20px' }}>
      <h3>产品列表</h3>
      {products.map(product => (
        <div 
          key={product.id}
          onClick={() => onSelectProduct(product)}
          style={{
            padding: '10px',
            margin: '5px 0',
            border: '1px solid #ccc',
            borderRadius: '4px',
            cursor: 'pointer',
            backgroundColor: '#f8f9fa'
          }}
        >
          <div style={{ fontWeight: 'bold' }}>{product.name}</div>
          <div style={{ color: '#666' }}>¥{product.price}</div>
        </div>
      ))}
    </div>
  );
}

// 产品详情组件
function ProductDetail({ product }) {
  if (!product) {
    return (
      <div style={{ width: '400px', border: '1px solid #ddd', padding: '20px' }}>
        <h3>产品详情</h3>
        <p>请选择一个产品查看详情</p>
      </div>
    );
  }
  
  return (
    <div style={{ width: '400px', border: '1px solid #ddd', padding: '20px' }}>
      <h3>产品详情</h3>
      <img 
        src={product.image || '/placeholder.jpg'} 
        alt={product.name}
        style={{ width: '100%', height: '200px', objectFit: 'cover', marginBottom: '15px' }}
      />
      <h4>{product.name}</h4>
      <p style={{ fontSize: '24px', color: '#007bff', fontWeight: 'bold' }}>
        ¥{product.price}
      </p>
      <p><strong>分类:</strong> {product.category}</p>
      <p><strong>描述:</strong> {product.description}</p>
      <p><strong>库存:</strong> {product.stock} 件</p>
      
      <button 
        style={{ 
          padding: '10px 20px', 
          backgroundColor: '#007bff', 
          color: 'white', 
          border: 'none', 
          borderRadius: '4px',
          width: '100%'
        }}
        disabled={product.stock === 0}
      >
        {product.stock > 0 ? '加入购物车' : '缺货'}
      </button>
    </div>
  );
}

// 父组件:管理兄弟组件通信
function ProductCatalog() {
  const [products] = React.useState([
    {
      id: 1,
      name: 'iPhone 15',
      price: 5999,
      category: '智能手机',
      description: '最新的iPhone,配备A17芯片',
      stock: 10,
      image: '/products/iphone15.jpg'
    },
    {
      id: 2,
      name: 'MacBook Pro',
      price: 12999,
      category: '笔记本电脑',
      description: '强大的专业级笔记本电脑',
      stock: 5,
      image: '/products/macbook.jpg'
    },
    {
      id: 3,
      name: 'AirPods Pro',
      price: 1899,
      category: '耳机',
      description: '主动降噪无线耳机',
      stock: 0,
      image: '/products/airpods.jpg'
    }
  ]);
  
  const [selectedProduct, setSelectedProduct] = React.useState(null);
  
  return (
    <div>
      <h2>产品目录</h2>
      <div style={{ display: 'flex', gap: '20px' }}>
        <ProductList 
          products={products} 
          onSelectProduct={setSelectedProduct}
        />
        <ProductDetail product={selectedProduct} />
      </div>
    </div>
  );
}

🌐 Context 跨层级通信

创建和使用 Context

jsx
// 创建主题 Context
const ThemeContext = React.createContext();

// 创建用户 Context
const UserContext = React.createContext();

// 主题提供者组件
function ThemeProvider({ children }) {
  const [theme, setTheme] = React.useState('light');
  
  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };
  
  const themeStyles = {
    light: {
      backgroundColor: '#ffffff',
      color: '#000000',
      borderColor: '#dddddd'
    },
    dark: {
      backgroundColor: '#2d3748',
      color: '#ffffff',
      borderColor: '#4a5568'
    }
  };
  
  return (
    <ThemeContext.Provider value={{
      theme,
      toggleTheme,
      styles: themeStyles[theme]
    }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 用户提供者组件
function UserProvider({ children }) {
  const [user, setUser] = React.useState(null);
  const [isLoading, setIsLoading] = React.useState(false);
  
  const login = async (credentials) => {
    setIsLoading(true);
    try {
      // 模拟登录 API
      await new Promise(resolve => setTimeout(resolve, 1000));
      setUser({
        id: 1,
        name: credentials.username,
        email: `${credentials.username}@example.com`,
        role: 'user'
      });
    } catch (error) {
      console.error('登录失败:', error);
    } finally {
      setIsLoading(false);
    }
  };
  
  const logout = () => {
    setUser(null);
  };
  
  return (
    <UserContext.Provider value={{
      user,
      isLoading,
      login,
      logout,
      isAuthenticated: !!user
    }}>
      {children}
    </UserContext.Provider>
  );
}

// 自定义 Hooks
function useTheme() {
  const context = React.useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

function useUser() {
  const context = React.useContext(UserContext);
  if (!context) {
    throw new Error('useUser must be used within a UserProvider');
  }
  return context;
}

// 头部组件
function Header() {
  const { theme, toggleTheme, styles } = useTheme();
  const { user, logout, isAuthenticated } = useUser();
  
  return (
    <header style={{
      ...styles,
      padding: '20px',
      borderBottom: `1px solid ${styles.borderColor}`,
      display: 'flex',
      justifyContent: 'space-between',
      alignItems: 'center'
    }}>
      <h1>我的应用</h1>
      
      <div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
        <button onClick={toggleTheme} style={{
          backgroundColor: styles.backgroundColor,
          color: styles.color,
          border: `1px solid ${styles.borderColor}`,
          padding: '8px 16px',
          borderRadius: '4px'
        }}>
          {theme === 'light' ? '🌙' : '☀️'} {theme === 'light' ? '深色' : '浅色'}
        </button>
        
        {isAuthenticated ? (
          <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
            <span>欢迎, {user.name}</span>
            <button onClick={logout} style={{
              backgroundColor: '#dc3545',
              color: 'white',
              border: 'none',
              padding: '8px 16px',
              borderRadius: '4px'
            }}>
              登出
            </button>
          </div>
        ) : (
          <LoginForm />
        )}
      </div>
    </header>
  );
}

// 登录表单组件
function LoginForm() {
  const [credentials, setCredentials] = React.useState({
    username: '',
    password: ''
  });
  const { login, isLoading } = useUser();
  const { styles } = useTheme();
  
  const handleSubmit = (e) => {
    e.preventDefault();
    login(credentials);
  };
  
  return (
    <form onSubmit={handleSubmit} style={{ display: 'flex', gap: '10px' }}>
      <input
        type="text"
        placeholder="用户名"
        value={credentials.username}
        onChange={(e) => setCredentials({ ...credentials, username: e.target.value })}
        style={{
          padding: '8px',
          backgroundColor: styles.backgroundColor,
          color: styles.color,
          border: `1px solid ${styles.borderColor}`,
          borderRadius: '4px'
        }}
      />
      <input
        type="password"
        placeholder="密码"
        value={credentials.password}
        onChange={(e) => setCredentials({ ...credentials, password: e.target.value })}
        style={{
          padding: '8px',
          backgroundColor: styles.backgroundColor,
          color: styles.color,
          border: `1px solid ${styles.borderColor}`,
          borderRadius: '4px'
        }}
      />
      <button 
        type="submit" 
        disabled={isLoading}
        style={{
          backgroundColor: '#007bff',
          color: 'white',
          border: 'none',
          padding: '8px 16px',
          borderRadius: '4px'
        }}
      >
        {isLoading ? '登录中...' : '登录'}
      </button>
    </form>
  );
}

// 主内容组件
function MainContent() {
  const { styles } = useTheme();
  const { isAuthenticated, user } = useUser();
  
  return (
    <main style={{
      ...styles,
      padding: '20px',
      minHeight: '400px'
    }}>
      {isAuthenticated ? (
        <div>
          <h2>欢迎回来,{user.name}!</h2>
          <p>这是受保护的内容区域。</p>
          <UserProfile />
        </div>
      ) : (
        <div>
          <h2>请先登录</h2>
          <p>登录后可以查看更多内容。</p>
        </div>
      )}
    </main>
  );
}

// 用户资料组件
function UserProfile() {
  const { user } = useUser();
  const { styles } = useTheme();
  
  return (
    <div style={{
      ...styles,
      border: `1px solid ${styles.borderColor}`,
      borderRadius: '8px',
      padding: '20px',
      marginTop: '20px'
    }}>
      <h3>用户资料</h3>
      <p><strong>姓名:</strong> {user.name}</p>
      <p><strong>邮箱:</strong> {user.email}</p>
      <p><strong>角色:</strong> {user.role}</p>
    </div>
  );
}

// 应用根组件
function ContextApp() {
  return (
    <ThemeProvider>
      <UserProvider>
        <div>
          <Header />
          <MainContent />
        </div>
      </UserProvider>
    </ThemeProvider>
  );
}

📝 本章小结

通过本章学习,你应该掌握了:

通信方式

  • ✅ 父子组件通信:Props 和回调函数
  • ✅ 兄弟组件通信:通过共同父组件
  • ✅ 跨层级通信:Context API
  • ✅ 复杂状态管理:状态提升模式

最佳实践

  1. 单向数据流:保持数据流向清晰
  2. 状态提升:将共享状态提升到最近的共同父组件
  3. Context 使用:适用于跨层级的全局状态
  4. 组件解耦:通过接口而非实现进行通信
  5. 性能优化:使用 memo 和 useCallback 优化渲染

继续学习下一章 - React 条件判断

本站内容仅供学习和研究使用。