React 组件间数据传递
1. Props 基本概念
组件化让我们可以构建可复用的 UI 模块,但这些模块需要一种方式来相互通信。props
(properties 的缩写) 正是 React 中实现组件间通信的基石,它使得组件变得更加动态和灵活。
Props 是从父组件传递给子组件的任意数据。可以把 props 想象成函数的参数,它们决定了组件的渲染输出和行为。
单向数据流
在 React 中,数据流是单向的 (Unidirectional Data Flow),即从父组件流向子组件。这意味着:
- 父组件可以通过 props 将数据或函数传递给子组件。
- 子组件可以读取和使用这些 props。
- 子组件不能直接修改接收到的 props。
这种自上而下的数据流使得应用的行为更容易预测和调试,因为数据的来源是清晰的。
Props 只读属性
一个组件绝不能修改它自己接收到的 props。无论一个组件是函数式的还是类式的,它都必须像一个纯函数一样对待它的 props,即对于相同的输入(props),总是返回相同的输出(UI),并且不产生任何副作用(不修改 props)。
// 错误!子组件试图修改自己的 props
function Greeting({ name }) {
name = 'New Name'; // 这是一个严重的反模式!
return <h1>Hello, {name}</h1>;
}
如果一个组件需要根据用户交互或其他原因来改变某些数据,它应该通过“状态(state)”来实现,这将在后面详细讲解·。
2. 传递 Props
传递与读取 Props
传递 props 的语法类似于 HTML 的属性。在父组件中,我们可以像给 HTML 标签添加属性一样,给自定义的组件添加 props。子组件会接收到一个名为 props
的对象,其中包含了所有传递过来的属性。
// 父组件 App.js
import Avatar from './Avatar';
export default function App() {
return (
<Avatar
username="Ada Lovelace"
avatarUrl="https://i.imgur.com/7vQD0fPs.jpg"
size={100}
/>
);
}
// 子组件 Avatar.js
function Avatar(props) {
return (
<img
src={props.avatarUrl}
alt={props.username}
width={props.size}
height={props.size}
/>
);
}
使用解构赋值简化 Props
为了让代码更简洁,通常的做法是在函数参数中直接对 props
对象进行解构。
// 直接在参数中解构,代码更清晰
function Avatar({ username, avatarUrl, size }) {
return (
<img
src={avatarUrl}
alt={username}
width={size}
height={size}
/>
);
}
设置默认 Props
为了防止因 props 未定义而导致的错误,并减少重复代码,我们可以为 props 设置默认值。推荐的方式是使用 ES6 的默认参数。
function Button({ text = "Click Me!", color = "blue" }) {
// ...
}
// 使用时,如果没有提供 text 或 color,它们会自动使用默认值
<Button /> // text="Click Me!", color="blue"
<Button text="Submit" /> // text="Submit", color="blue"
特殊的 Prop:children
children
是一个特殊的 prop,它代表了组件标签内部嵌套的内容。这使得创建“容器”或“包装”组件变得非常容易。
// 父组件
import Card from './Card';
function App() {
return (
<Card title="Welcome">
{/* 下面这些内容都会被作为 children prop 传递给 Card 组件 */}
<p>This is some content inside the card.</p>
<button>Click here</button>
</Card>
);
}
// Card 组件
function Card({ title, children }) {
return (
<div className="card">
<h1 className="card-title">{title}</h1>
<div className="card-content">
{children} {/* 在这里渲染所有嵌套的内容 */}
</div>
</div>
);
}
3. 将函数作为 Props 传递
由于 props 可以是任何 JavaScript 值,我们自然也可以将函数作为 props 传递。这是实现子组件向父组件通信的标准模式。
实现子组件向父组件通信
这个模式通常被称为“提升状态(Lifting State Up)”。其核心思想是:
- 父组件持有状态(state)和修改状态的函数。
- 父组件将修改状态的函数作为 prop 传递给子组件。
- 子组件在需要时(例如,用户点击按钮)调用从 prop 接收到的函数。
- 父组件中的函数被执行,更新父组件的状态,从而导致 UI 重新渲染。
传递函数引用与调用
// 父组件
function App() {
const handleButtonClick = () => {
alert('Button was clicked in the child!');
};
return (
<div>
{/* 传递函数引用,而不是调用它 */}
<Button onButtonClick={handleButtonClick} />
</div>
);
}
// 子组件
function Button({ onButtonClick }) {
return (
// 在 onClick 事件中调用从 props 接收的函数
<button onClick={onButtonClick}>
Click Me
</button>
);
}
关键点:当需要从子组件向父组件传递数据时(例如,被点击项的 ID),应该使用一个匿名箭头函数来包装调用。
// 父组件
const handleItemClick = (itemId) => {
alert(`Item ${itemId} was clicked!`);
};
// 子组件中
<li onClick={() => onListItemClick(item.id)}>{item.name}</li>
直接写成 onClick={onListItemClick(item.id)}
是错误的,因为这会在组件渲染时立即调用函数,而不是等到点击时才调用。而这个函数没有返回值,这会导致组件的处理函数变成 undefined。
5. 属性钻探
当一个深层嵌套的子组件需要某个 prop,而这个 prop 必须穿过多个本身并不需要该 prop 的中间组件时,这种现象被称为属性钻探 (Prop Drilling) 。
<App data={someData}>
<Layout>
<Sidebar>
<Profile data={someData} /> {/* data 穿过了 Layout 和 Sidebar */}
</Sidebar>
</Layout>
</App>
对于层级不深的应用,这是完全正常的。但当层级非常深时,它会使得组件之间的耦合度增高,难以维护。为了解决这个问题,React 提供了更高级的工具,如 Context API,或者可以使用像 Redux 这样的状态管理库。