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
- ✅ 复杂状态管理:状态提升模式
最佳实践
- 单向数据流:保持数据流向清晰
- 状态提升:将共享状态提升到最近的共同父组件
- Context 使用:适用于跨层级的全局状态
- 组件解耦:通过接口而非实现进行通信
- 性能优化:使用 memo 和 useCallback 优化渲染
继续学习:下一章 - React 条件判断