Dark Dwarf Blog background

React 组件间数据传递

React 组件间数据传递

1. Props 基本概念

组件化让我们可以构建可复用的 UI 模块,但这些模块需要一种方式来相互通信。props (properties 的缩写) 正是 React 中实现组件间通信的基石,它使得组件变得更加动态和灵活。

Props 是从父组件传递给子组件的任意数据。可以把 props 想象成函数的参数,它们决定了组件的渲染输出和行为。

a.a. 单向数据流

在 React 中,数据流是单向的 (Unidirectional Data Flow),即从父组件流向子组件。这意味着:

  • 父组件可以通过 props 将数据或函数传递给子组件。
  • 子组件可以读取和使用这些 props。
  • 子组件不能直接修改接收到的 props。

这种自上而下的数据流使得应用的行为更容易预测和调试,因为数据的来源是清晰的。

b.b. Props 只读属性

一个组件绝不能修改它自己接收到的 props。无论一个组件是函数式的还是类式的,它都必须像一个纯函数一样对待它的 props,即对于相同的输入(props),总是返回相同的输出(UI),并且不产生任何副作用(不修改 props)。

// 错误!子组件试图修改自己的 props
function Greeting({ name }) {
  name = 'New Name'; // 这是一个严重的反模式!
  return <h1>Hello, {name}</h1>;
}

如果一个组件需要根据用户交互或其他原因来改变某些数据,它应该通过“状态(state)”来实现,这将在后面详细讲解·。

2. 传递 Props

a.a. 传递与读取 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}
    />
  );
}

b.b. 使用解构赋值简化 Props

为了让代码更简洁,通常的做法是在函数参数中直接对 props 对象进行解构。

// 直接在参数中解构,代码更清晰
function Avatar({ username, avatarUrl, size }) {
  return (
    <img
      src={avatarUrl}
      alt={username}
      width={size}
      height={size}
    />
  );
}

c.c. 设置默认 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"

d.d. 特殊的 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 传递。这是实现子组件向父组件通信的标准模式。

a.a. 实现子组件向父组件通信

这个模式通常被称为“提升状态(Lifting State Up)”。其核心思想是:

  1. 父组件持有状态(state)和修改状态的函数。
  2. 父组件将修改状态的函数作为 prop 传递给子组件。
  3. 子组件在需要时(例如,用户点击按钮)调用从 prop 接收到的函数。
  4. 父组件中的函数被执行,更新父组件的状态,从而导致 UI 重新渲染。

b.b. 传递函数引用与调用

// 父组件
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 这样的状态管理库。