06-React-Props

06、React Props

从入门到企业级实战,一篇文章彻底搞懂 React 组件通信的基石

在 React 的世界里,组件是构建用户界面的基本单位。而组件之间如何对话、如何传递数据、如何协同工作,答案几乎都绕不开一个核心概念------Props。

无论是刚入门的小白,还是工作多年的前端工程师,Props 都是每天都在使用的东西。但真正能把 Props 讲透彻、用到位、在面试中对答如流的人,其实并不多。

这篇文章将从真实项目中的痛点出发,带你从零到一彻底掌握 React Props,涵盖核心概念、演进历史、基础用法、企业级实战、竞品对比、常用场景以及面试高频考点,让你看完就能直接上手。


文章目录

  • [06、React Props](#06、React Props)
    • 一、痛点场景:你是否也被这些问题困扰过?
      • [1.1 Prop Drilling:数据要穿越八层组件](#1.1 Prop Drilling:数据要穿越八层组件)
      • [1.2 子组件想修改父组件的数据,怎么办?](#1.2 子组件想修改父组件的数据,怎么办?)
      • [1.3 组件复用困难,每个地方都要写一遍](#1.3 组件复用困难,每个地方都要写一遍)
      • [1.4 类型不明确,传错了才发现](#1.4 类型不明确,传错了才发现)
    • [二、痛点的解决方案:认识 Props](#二、痛点的解决方案:认识 Props)
      • [2.1 专业解释](#2.1 专业解释)
      • [2.2 大白话解释](#2.2 大白话解释)
      • [2.3 生活案例](#2.3 生活案例)
    • [三、为什么要用 Props?](#三、为什么要用 Props?)
      • [3.1 组件复用的基础](#3.1 组件复用的基础)
      • [3.2 单向数据流保证可预测性](#3.2 单向数据流保证可预测性)
      • [3.3 组件解耦](#3.3 组件解耦)
      • [3.4 性能优化的基础](#3.4 性能优化的基础)
    • [四、Props 是怎么演进过来的?](#四、Props 是怎么演进过来的?)
      • [4.1 上古时代:React.createClass(2013)](#4.1 上古时代:React.createClass(2013))
      • [4.2 中古时代:ES6 Class 组件(2015)](#4.2 中古时代:ES6 Class 组件(2015))
      • [4.3 近代:函数组件 + Hooks(2018)](#4.3 近代:函数组件 + Hooks(2018))
      • [4.4 现代:TypeScript 类型定义(2020 至今)](#4.4 现代:TypeScript 类型定义(2020 至今))
      • [4.5 React 19 的变化(2024)](#4.5 React 19 的变化(2024))
    • [五、怎么用 Props?](#五、怎么用 Props?)
      • [5.1 基础用法四步走](#5.1 基础用法四步走)
      • [5.2 子传父:通过回调函数](#5.2 子传父:通过回调函数)
      • [5.3 children:特殊的 props](#5.3 children:特殊的 props)
      • [5.4 展开运算符:批量传递 props](#5.4 展开运算符:批量传递 props)
    • [六、企业项目中如何使用 Props?](#六、企业项目中如何使用 Props?)
      • [6.1 组件设计原则:容器组件与展示组件分离](#6.1 组件设计原则:容器组件与展示组件分离)
      • [6.2 解决 Prop Drilling:组合优于继承](#6.2 解决 Prop Drilling:组合优于继承)
      • [6.3 Render Props 模式](#6.3 Render Props 模式)
      • [6.4 性能优化:避免不必要的重渲染](#6.4 性能优化:避免不必要的重渲染)
      • [6.5 类型安全:TypeScript 最佳实践](#6.5 类型安全:TypeScript 最佳实践)
    • [七、竞品对比:Props vs State vs Context vs Redux](#七、竞品对比:Props vs State vs Context vs Redux)
      • [7.1 选型决策树](#7.1 选型决策树)
      • [7.2 React Props vs Vue Props](#7.2 React Props vs Vue Props)
    • 八、常用场景大全
      • [8.1 场景一:父传子数据传递](#8.1 场景一:父传子数据传递)
      • [8.2 场景二:子传父回调通信](#8.2 场景二:子传父回调通信)
      • [8.3 场景三:组件复用配置化](#8.3 场景三:组件复用配置化)
      • [8.4 场景四:条件渲染控制](#8.4 场景四:条件渲染控制)
      • [8.5 场景五:样式与主题定制](#8.5 场景五:样式与主题定制)
      • [8.6 场景六:表单数据双向绑定](#8.6 场景六:表单数据双向绑定)
    • 九、面试官高频面试题
      • [面试题 1:什么是 Props?它有什么特点?](#面试题 1:什么是 Props?它有什么特点?)
      • [面试题 2:Props 和 State 的区别是什么?](#面试题 2:Props 和 State 的区别是什么?)
      • [面试题 3:为什么 Props 是不可变的?](#面试题 3:为什么 Props 是不可变的?)
      • [面试题 4:子组件如何修改父组件的数据?](#面试题 4:子组件如何修改父组件的数据?)
      • [面试题 5:什么是 Prop Drilling?如何解决?](#面试题 5:什么是 Prop Drilling?如何解决?)
      • [面试题 6:React.memo 有什么用?它的原理是什么?](#面试题 6:React.memo 有什么用?它的原理是什么?)
      • [面试题 7:defaultProps 和参数默认值有什么区别?](#面试题 7:defaultProps 和参数默认值有什么区别?)
      • [面试题 8:children 是什么?有哪些用法?](#面试题 8:children 是什么?有哪些用法?)
      • [面试题 9:如何对 Props 进行类型检查?](#面试题 9:如何对 Props 进行类型检查?)
      • [面试题 10:React 19 对 Props 做了哪些改变?](#面试题 10:React 19 对 Props 做了哪些改变?)
    • 十、总结与最佳实践清单
      • [10.1 核心要点回顾](#10.1 核心要点回顾)
      • [10.2 企业级最佳实践清单](#10.2 企业级最佳实践清单)
      • [10.3 学习路径建议](#10.3 学习路径建议)

一、痛点场景:你是否也被这些问题困扰过?

1.1 Prop Drilling:数据要穿越八层组件

想象一下这样的场景:你在一个中后台项目中,顶层的 App 组件获取了用户信息 userInfo,而最底层的一个 UserAvatar 组件需要展示用户头像。中间隔着 Layout、Sidebar、Menu、MenuItem 等四五层组件。

为了把 userInfo 传到最底层,你不得不让每一层中间组件都接收这个 props,再往下传。这些中间组件根本用不到 userInfo,它们只是充当了"快递员"的角色。

javascript 复制代码
// App.jsx - 顶层组件
function App() {
  const [userInfo, setUserInfo] = useState({ name: '张三', avatar: '/avatar.png' });
  return <Layout userInfo={userInfo} />;
}

// Layout.jsx - 只是转发,不使用 userInfo
function Layout({ userInfo }) {
  return (
    <div>
      <Sidebar userInfo={userInfo} />
      <Content />
    </div>
  );
}

// Sidebar.jsx - 还是转发
function Sidebar({ userInfo }) {
  return <Menu userInfo={userInfo} />;
}

// Menu.jsx - 继续转发
function Menu({ userInfo }) {
  return <MenuItem userInfo={userInfo} />;
}

// MenuItem.jsx - 终于用到了
function MenuItem({ userInfo }) {
  return <UserAvatar avatar={userInfo.avatar} />;
}

这就是经典的 Prop Drilling(属性钻探) 问题。代码冗余、维护困难、重构痛苦------改一个字段名要改八层文件。

1.2 子组件想修改父组件的数据,怎么办?

新手常犯的错误:直接在子组件里修改 props。

javascript 复制代码
// 错误示范
function Child({ count }) {
  // 报错:props 是只读的,不能直接修改
  count = count + 1;
  return <div>{count}</div>;
}

然后你会困惑:"我明明改了,为什么不生效?"或者"为什么控制台报 warning?"

1.3 组件复用困难,每个地方都要写一遍

你写了一个 Button 组件,在 A 页面要红色,在 B 页面要蓝色,在 C 页面要大号字体。如果不通过 props 配置化,你就得写三个 Button 组件,或者在组件内部写一堆 if-else 判断当前是哪个页面。

1.4 类型不明确,传错了才发现

父组件传了一个 string 类型的 age,子组件当成 number 来做数学运算,结果 NaN。没有类型检查,运行时才报错,调试半天。

这些痛点,几乎每一个 React 开发者都遇到过。而解决这些问题的钥匙,就是正确理解和使用 Props。


二、痛点的解决方案:认识 Props

2.1 专业解释

Props (Properties 的缩写)是 React 组件之间传递数据的机制。父组件通过 JSX 属性的形式将数据传递给子组件,子组件通过函数参数(函数组件)或 this.props(类组件)接收这些数据。

Props 的核心特征:

  • 单向数据流:数据只能从父组件流向子组件,不能反向流动
  • 只读性:子组件不能直接修改 props,所有 React 组件都必须像纯函数一样保护它们的 props 不被更改
  • 可传递任意类型:字符串、数字、布尔值、数组、对象、函数、甚至 JSX 元素都可以作为 props 传递

2.2 大白话解释

说人话:Props 就像是组件的"快递包裹"。

  • 父组件是"寄件人",把要传递的数据打包成包裹(props)
  • 子组件是"收件人",签收包裹后可以使用里面的东西
  • 但收件人不能修改包裹里的东西(只读),如果需要改,得联系寄件人(父组件)去改
  • 包裹里可以装任何东西:文字、数字、列表、甚至是一个"操作指令"(回调函数)

2.3 生活案例

去餐厅吃饭的场景:

  • 你(父组件)向服务员(子组件)下单,说"我要一份牛肉面,不要香菜,多加辣"
  • 这些要求(牛肉面、不要香菜、多加辣)就是 props
  • 服务员收到订单后,按照要求去后厨传达,但服务员不能私自更改你的订单(只读)
  • 如果服务员发现你点的菜卖完了,他会回来告诉你(通过回调函数通知父组件),由你来决定换什么菜
  • 同一家餐厅(同一个组件),不同的顾客(不同的父组件)可以点不同的菜(传不同的 props),这就是组件的复用性

三、为什么要用 Props?

3.1 组件复用的基础

Props 让组件变得可配置。同一个 Button 组件,通过传入不同的 props,可以变成不同的样子:

jsx 复制代码
<Button type="primary" size="large">提交</Button>
<Button type="danger" size="small">删除</Button>
<Button type="default" disabled>禁用</Button>

如果没有 props,你需要为每种按钮写一个单独的组件,代码量爆炸。

3.2 单向数据流保证可预测性

Props 的只读性和单向数据流,让数据的变化路径变得清晰可追溯。当界面出现问题时,你只需要沿着组件树向上找,就能定位到数据的源头。这比双向绑定的"谁都能改"模式更容易调试。

3.3 组件解耦

子组件不需要关心数据从哪里来、怎么来的,它只需要声明"我需要什么数据"(通过 props 接口),父组件负责提供。这种关注点分离,让组件可以独立开发、独立测试。

3.4 性能优化的基础

因为 props 是不可变的,React 可以通过浅比较(shallow comparison)快速判断 props 是否变化,从而决定是否重新渲染组件。React.memo、useMemo 等优化手段,都建立在 props 不可变这个前提之上。


四、Props 是怎么演进过来的?

React 从 2013 年发布至今,Props 的写法经历了几次重大变革。了解这段历史,能帮你看懂老项目的代码,也能理解为什么现在推荐函数组件的写法。

4.1 上古时代:React.createClass(2013)

最早的 React 使用 createClass 来创建组件,props 通过 this.props 访问,默认值用 getDefaultProps 方法:

javascript 复制代码
var HelloMessage = React.createClass({
  getDefaultProps: function() {
    return {
      name: '默认名称'
    };
  },
  render: function() {
    return <h1>Hello {this.props.name}</h1>;
  }
});

这种写法的问题:冗长、this 绑定麻烦、不支持 ES6 语法。

4.2 中古时代:ES6 Class 组件(2015)

React 0.13 引入了 ES6 Class 语法,props 的写法变得更现代化:

javascript 复制代码
class HelloMessage extends React.Component {
  render() {
    return <h1>Hello {this.props.name}</h1>;
  }
}

HelloMessage.defaultProps = {
  name: '默认名称'
};

同时引入了 PropTypes 进行类型检查:

javascript 复制代码
import PropTypes from 'prop-types';

HelloMessage.propTypes = {
  name: PropTypes.string.isRequired
};

4.3 近代:函数组件 + Hooks(2018)

React 16.8 引入 Hooks 后,函数组件成为主流。props 直接作为函数参数传入,可以用解构赋值简化:

javascript 复制代码
function HelloMessage({ name = '默认名称' }) {
  return <h1>Hello {name}</h1>;
}

默认值直接用 ES6 的参数默认值语法,不再需要 defaultProps。

4.4 现代:TypeScript 类型定义(2020 至今)

TypeScript 的普及让 props 的类型定义更加严谨:

typescript 复制代码
interface HelloMessageProps {
  name: string;
  age?: number;
}

function HelloMessage({ name, age = 18 }: HelloMessageProps) {
  return (
    <div>
      <h1>Hello {name}</h1>
      <p>Age: {age}</p>
    </div>
  );
}

4.5 React 19 的变化(2024)

React 19 对 props 做了两个重要调整:

  1. ref 成为普通 props :函数组件中可以直接从 props 中解构 ref,不再需要 forwardRef
  2. 移除函数组件的 defaultProps 和 propTypes:推荐使用参数默认值和 TypeScript
tsx 复制代码
// React 19 新写法
function MyInput({ ref, placeholder = '请输入' }) {
  return <input ref={ref} placeholder={placeholder} />;
}

五、怎么用 Props?

5.1 基础用法四步走

第一步:父组件定义数据
javascript 复制代码
// Parent.jsx
import { useState } from 'react';

function Parent() {
  const [message, setMessage] = useState('Hello from Parent');
  return <Child text={message} />;
}
第二步:通过 JSX 属性传递 props
javascript 复制代码
// 传递字符串
<Child text="Hello" />

// 传递变量
<Child text={message} />

// 传递数字
<Child count={10} />

// 传递布尔值
<Child visible={true} />
// 简写(等价于 visible={true})
<Child visible />

// 传递对象
<Child user={{ name: '张三', age: 25 }} />

// 传递数组
<Child list={[1, 2, 3]} />

// 传递函数
<Child onClick={() => console.log('clicked')} />

// 传递 JSX(children)
<Child>
  <p>这是 children</p>
</Child>
第三步:子组件接收 props
javascript 复制代码
// 方式一:直接使用 props 参数
function Child(props) {
  return <div>{props.text}</div>;
}

// 方式二:解构赋值(推荐)
function Child({ text }) {
  return <div>{text}</div>;
}

// 方式三:解构 + 默认值(更推荐)
function Child({ text = '默认文本', count = 0 }) {
  return (
    <div>
      <p>{text}</p>
      <p>{count}</p>
    </div>
  );
}

// 方式四:剩余参数收集
function Child({ text, ...rest }) {
  // rest 包含除了 text 之外的所有 props
  return <div {...rest}>{text}</div>;
}
第四步:子组件渲染使用
javascript 复制代码
function UserCard({ name, age, avatar }) {
  return (
    <div className="user-card">
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <p>年龄:{age} 岁</p>
    </div>
  );
}

5.2 子传父:通过回调函数

Props 是单向的,子组件不能直接修改父组件的数据。但子组件可以通过调用父组件传下来的回调函数,通知父组件去修改:

javascript 复制代码
// Parent.jsx
function Parent() {
  const [count, setCount] = useState(0);

  const handleIncrement = () => {
    setCount(prev => prev + 1);
  };

  return (
    <div>
      <p>当前计数:{count}</p>
      <Child onIncrement={handleIncrement} />
    </div>
  );
}

// Child.jsx
function Child({ onIncrement }) {
  return (
    <button onClick={onIncrement}>
      点击增加
    </button>
  );
}

如果子组件需要传递参数给父组件:

javascript 复制代码
// Child.jsx
function Child({ onSelect }) {
  const items = ['苹果', '香蕉', '橙子'];

  return (
    <ul>
      {items.map(item => (
        <li key={item} onClick={() => onSelect(item)}>
          {item}
        </li>
      ))}
    </ul>
  );
}

// Parent.jsx
function Parent() {
  const [selected, setSelected] = useState('');

  const handleSelect = (item) => {
    setSelected(item);
    console.log('选中了:', item);
  };

  return (
    <div>
      <p>已选择:{selected}</p>
      <Child onSelect={handleSelect} />
    </div>
  );
}

5.3 children:特殊的 props

children 是一个特殊的 prop,代表组件标签之间的内容:

javascript 复制代码
// 使用方式
<Card>
  <h2>标题</h2>
  <p>这是卡片内容</p>
</Card>

// Card 组件接收 children
function Card({ children, title }) {
  return (
    <div className="card">
      {title && <h3 className="card-title">{title}</h3>}
      <div className="card-body">{children}</div>
    </div>
  );
}

children 可以是任何东西:字符串、数字、JSX 元素、数组、甚至函数(Render Props 模式)。

5.4 展开运算符:批量传递 props

当需要传递的 props 很多时,可以用展开运算符简化:

javascript 复制代码
function Parent() {
  const config = {
    name: '张三',
    age: 25,
    gender: '男',
    address: '广州市天河区'
  };

  // 等价于 <User name={config.name} age={config.age} ... />
  return <User {...config} />;
}

六、企业项目中如何使用 Props?

理论学完了,我们来看真实企业项目中的 Props 使用模式和最佳实践。

6.1 组件设计原则:容器组件与展示组件分离

企业项目中通常遵循"容器组件 + 展示组件"的模式:

  • 容器组件(Container):负责数据获取、状态管理、业务逻辑,通过 props 把数据传给展示组件
  • 展示组件(Presentational):只负责根据 props 渲染 UI,不关心数据来源
typescript 复制代码
// containers/UserListContainer.tsx - 容器组件
import { useState, useEffect } from 'react';
import UserList from '../components/UserList';

interface User {
  id: number;
  name: string;
  email: string;
}

export default function UserListContainer() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetchUsers();
  }, []);

  const fetchUsers = async () => {
    try {
      setLoading(true);
      const res = await fetch('/api/users');
      const data = await res.json();
      setUsers(data);
    } catch (err) {
      setError('获取用户列表失败');
    } finally {
      setLoading(false);
    }
  };

  const handleDelete = async (id: number) => {
    await fetch(`/api/users/${id}`, { method: 'DELETE' });
    setUsers(prev => prev.filter(u => u.id !== id));
  };

  return (
    <UserList
      users={users}
      loading={loading}
      error={error}
      onDelete={handleDelete}
      onRefresh={fetchUsers}
    />
  );
}
typescript 复制代码
// components/UserList.tsx - 展示组件
interface User {
  id: number;
  name: string;
  email: string;
}

interface UserListProps {
  users: User[];
  loading: boolean;
  error: string | null;
  onDelete: (id: number) => void;
  onRefresh: () => void;
}

export default function UserList({
  users,
  loading,
  error,
  onDelete,
  onRefresh
}: UserListProps) {
  if (loading) return <div className="loading">加载中...</div>;
  if (error) return <div className="error">{error}</div>;

  return (
    <div className="user-list">
      <button onClick={onRefresh}>刷新</button>
      <table>
        <thead>
          <tr>
            <th>ID</th>
            <th>姓名</th>
            <th>邮箱</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          {users.map(user => (
            <tr key={user.id}>
              <td>{user.id}</td>
              <td>{user.name}</td>
              <td>{user.email}</td>
              <td>
                <button onClick={() => onDelete(user.id)}>删除</button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

6.2 解决 Prop Drilling:组合优于继承

面对 Prop Drilling,不一定非要用 Context 或 Redux。很多时候,组件组合(Composition) 是更优雅的解决方案:

tsx 复制代码
// 不好的做法:层层传递
function Page() {
  const user = useUser();
  return <Layout user={user} />;
}

function Layout({ user }) {
  return <Sidebar user={user} />;
}

function Sidebar({ user }) {
  return <UserPanel user={user} />;
}

// 好的做法:组合模式
function Page() {
  const user = useUser();
  return (
    <Layout
      sidebar={
        <Sidebar>
          <UserPanel user={user} />
        </Sidebar>
      }
    />
  );
}

function Layout({ sidebar, content }) {
  return (
    <div className="layout">
      <aside>{sidebar}</aside>
      <main>{content}</main>
    </div>
  );
}

通过把已经组装好的组件作为 children 或具名 prop 传入,中间层组件完全不需要知道 user 的存在。

6.3 Render Props 模式

Render Props 是一种通过函数类型的 prop 来共享代码的模式:

tsx 复制代码
// MouseTracker.tsx - 追踪鼠标位置的通用组件
import { useState, useEffect } from 'react';

interface Position {
  x: number;
  y: number;
}

interface MouseTrackerProps {
  render: (position: Position) => React.ReactNode;
}

export default function MouseTracker({ render }: MouseTrackerProps) {
  const [position, setPosition] = useState<Position>({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };
    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, []);

  return <div>{render(position)}</div>;
}

// 使用方式
function App() {
  return (
    <MouseTracker
      render={({ x, y }) => (
        <div>
          鼠标位置:({x}, {y})
        </div>
      )}
    />
  );
}

6.4 性能优化:避免不必要的重渲染

企业项目中,props 的使用方式直接影响性能。以下是几个关键优化点:

tsx 复制代码
// 1. 使用 React.memo 包裹纯展示组件
const UserCard = React.memo(function UserCard({ user }: { user: User }) {
  return <div>{user.name}</div>;
});

// 2. 避免在渲染中创建新对象/新函数(会导致 memo 失效)
// 不好的写法
function Parent() {
  const [count, setCount] = useState(0);
  return (
    <Child
      config={{ theme: 'dark' }}  // 每次渲染都是新对象
      onClick={() => doSomething()}  // 每次渲染都是新函数
    />
  );
}

// 好的写法
function Parent() {
  const [count, setCount] = useState(0);
  const config = useMemo(() => ({ theme: 'dark' }), []);
  const handleClick = useCallback(() => doSomething(), []);

  return <Child config={config} onClick={handleClick} />;
}

// 3. 大列表使用虚拟滚动,避免传递大量 props

6.5 类型安全:TypeScript 最佳实践

typescript 复制代码
// 定义 Props 类型(推荐用 interface)
interface ButtonProps {
  /** 按钮类型 */
  type?: 'primary' | 'default' | 'danger';
  /** 按钮尺寸 */
  size?: 'small' | 'medium' | 'large';
  /** 点击事件 */
  onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
  /** 是否禁用 */
  disabled?: boolean;
  /** 子元素 */
  children: React.ReactNode;
}

// 函数组件 + 泛型(更严谨)
function Button({
  type = 'default',
  size = 'medium',
  disabled = false,
  onClick,
  children
}: ButtonProps) {
  return (
    <button
      className={`btn btn-${type} btn-${size}`}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
}

// 继承原生元素属性
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary';
}

function Button({ variant = 'primary', ...rest }: ButtonProps) {
  return <button className={`btn-${variant}`} {...rest} />;
}

七、竞品对比:Props vs State vs Context vs Redux

在 React 生态中,数据管理有多种方案。很多新手分不清什么时候该用什么。下面这张表帮你一次性理清:

对比维度 Props State Context Redux / Zustand
数据来源 父组件传入 组件内部创建 Provider 提供 全局 Store 管理
可变性 只读,不可修改 可变,setState 更新 通过 Provider value 更新 通过 dispatch / action 更新
作用范围 父子组件之间 当前组件内部 组件树任意层级 全局任意组件
适用场景 组件配置、数据传递、回调通信 组件内部交互状态(输入框、开关、弹窗) 主题、语言、用户信息等跨层级共享 大型应用全局状态、复杂状态逻辑
学习成本 低 低 中 高(Redux)/ 中(Zustand)
性能影响 浅比较即可优化 直接触发重渲染 value 变化导致所有 Consumer 重渲染 可精确订阅,按需重渲染
调试难度 低,数据流清晰 低,局部可控 中,需追踪 Provider 中高,但有 DevTools 支持
代码量 少 少 中等 多(Redux)/ 少(Zustand)
典型反模式 Prop Drilling 把所有状态都放 state 滥用 Context 代替 Props 什么状态都往全局放

7.1 选型决策树

复制代码
需要共享数据吗?
├── 不需要 → 用 State(组件内部状态)
└── 需要
    ├── 只在父子之间传递?
    │   └── 是 → 用 Props
    └── 需要跨多层或多分支共享?
        ├── 数据变化不频繁(主题、语言、用户信息)?
        │   └── 是 → 用 Context
        └── 数据复杂、变化频繁、需要中间件?
            └── 是 → 用 Redux / Zustand

7.2 React Props vs Vue Props

如果你有 Vue 背景,转到 React 时最容易困惑的就是 Props 的差异:

对比维度 React Props Vue Props
声明方式 函数参数 / this.props,无需声明 必须在 props 选项或 defineProps 中声明
子传父 传递回调函数作为 prop $emit 触发事件,父组件 @event 监听
类型检查 TypeScript / PropTypes 内置类型校验(type、required、default、validator)
默认值 函数参数默认值 / defaultProps default 字段
双向绑定 不支持,需手动传回调 支持 v-model 语法糖
事件命名 onClick、onChange(驼峰) @click、@change(短横线)
插槽 children prop slot 具名插槽
设计哲学 一切皆 JS,用函数和组合表达 模板语法,框架提供更多约定
vue 复制代码
<!-- Vue 写法 -->
<template>
  <Child :message="msg" @update="handleUpdate" />
</template>

<script setup>
import { ref } from 'vue';
const msg = ref('Hello');
const handleUpdate = (val) => { msg.value = val; };
</script>
jsx 复制代码
// React 写法
function Parent() {
  const [msg, setMsg] = useState('Hello');
  return <Child message={msg} onUpdate={setMsg} />;
}

八、常用场景大全

8.1 场景一:父传子数据传递

最基础的用法,把数据从父组件传到子组件展示:

tsx 复制代码
function ProductList() {
  const products = [
    { id: 1, name: 'iPhone 15', price: 5999 },
    { id: 2, name: 'MacBook Pro', price: 14999 },
  ];

  return (
    <div>
      {products.map(product => (
        <ProductCard
          key={product.id}
          name={product.name}
          price={product.price}
        />
      ))}
    </div>
  );
}

8.2 场景二:子传父回调通信

子组件通过回调函数通知父组件发生了什么:

tsx 复制代码
// 搜索框子组件
function SearchInput({ onSearch }) {
  const [keyword, setKeyword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    onSearch(keyword);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={keyword}
        onChange={e => setKeyword(e.target.value)}
        placeholder="搜索..."
      />
      <button type="submit">搜索</button>
    </form>
  );
}

// 父组件使用
function Page() {
  const handleSearch = (keyword) => {
    console.log('搜索关键词:', keyword);
    // 调用搜索 API
  };

  return <SearchInput onSearch={handleSearch} />;
}

8.3 场景三:组件复用配置化

通过 props 让同一个组件适应不同场景:

tsx 复制代码
interface ModalProps {
  visible: boolean;
  title: string;
  confirmText?: string;
  cancelText?: string;
  onConfirm: () => void;
  onCancel: () => void;
  children: React.ReactNode;
}

function Modal({
  visible,
  title,
  confirmText = '确定',
  cancelText = '取消',
  onConfirm,
  onCancel,
  children
}: ModalProps) {
  if (!visible) return null;

  return (
    <div className="modal-overlay">
      <div className="modal">
        <h3>{title}</h3>
        <div className="modal-body">{children}</div>
        <div className="modal-footer">
          <button onClick={onCancel}>{cancelText}</button>
          <button className="primary" onClick={onConfirm}>{confirmText}</button>
        </div>
      </div>
    </div>
  );
}

// 不同场景复用同一个 Modal
<Modal
  visible={deleteVisible}
  title="确认删除"
  confirmText="删除"
  onConfirm={handleDelete}
  onCancel={() => setDeleteVisible(false)}
>
  <p>确定要删除这条记录吗?此操作不可恢复。</p>
</Modal>

<Modal
  visible={infoVisible}
  title="提示"
  confirmText="我知道了"
  onConfirm={() => setInfoVisible(false)}
  onCancel={() => setInfoVisible(false)}
>
  <p>操作成功!</p>
</Modal>

8.4 场景四:条件渲染控制

通过 props 控制组件的显示逻辑:

tsx 复制代码
function Page({ isAdmin, isVip, user }) {
  return (
    <div>
      {isAdmin && <AdminPanel />}
      {isVip ? <VipContent /> : <FreeContent />}
      {user && <UserProfile user={user} />}
    </div>
  );
}

8.5 场景五:样式与主题定制

tsx 复制代码
interface BadgeProps {
  count: number;
  color?: 'red' | 'blue' | 'green';
  max?: number;
}

function Badge({ count, color = 'red', max = 99 }: BadgeProps) {
  const display = count > max ? `${max}+` : count;
  return <span className={`badge badge-${color}`}>{display}</span>;
}

// 使用
<Badge count={5} />
<Badge count={150} color="blue" max={99} />

8.6 场景六:表单数据双向绑定

React 没有 v-model,需要通过 value + onChange 实现:

tsx 复制代码
function Form() {
  const [form, setForm] = useState({ username: '', email: '' });

  const handleChange = (field) => (e) => {
    setForm(prev => ({ ...prev, [field]: e.target.value }));
  };

  return (
    <form>
      <Input
        label="用户名"
        value={form.username}
        onChange={handleChange('username')}
      />
      <Input
        label="邮箱"
        value={form.email}
        onChange={handleChange('email')}
      />
    </form>
  );
}

function Input({ label, value, onChange }) {
  return (
    <div className="form-item">
      <label>{label}</label>
      <input value={value} onChange={onChange} />
    </div>
  );
}

九、面试官高频面试题

面试题 1:什么是 Props?它有什么特点?

参考答案:

Props 是 React 中组件之间传递数据的机制,全称 Properties。父组件通过 JSX 属性将数据传递给子组件,子组件通过函数参数或 this.props 接收。

核心特点:

  1. 单向数据流:数据只能从父到子,不能反向
  2. 只读性:子组件不能修改 props,必须像纯函数一样保护 props
  3. 可传递任意类型:包括基本类型、对象、数组、函数、JSX 等
  4. 触发重渲染:props 变化会导致组件重新渲染

面试题 2:Props 和 State 的区别是什么?

参考答案:

维度 Props State
来源 父组件传入 组件内部创建
可变性 只读,不可修改 可变,通过 setState 更新
作用 组件间通信、配置化 组件内部状态管理
重渲染 父组件传入新值时触发 setState 调用时触发
类比 函数参数 函数内部变量

一句话总结:Props 是外部传入的配置,State 是内部管理的状态。

面试题 3:为什么 Props 是不可变的?

参考答案:

  1. 可预测性:单向数据流让状态变更路径清晰可追溯,避免"幽灵更新",简化调试
  2. 性能优化 :不可变性让 React 可以通过浅比较高效判断 props 是否变化,配合 React.memo 实现跳过渲染
  3. 可维护性:降低组件耦合度,子组件不会意外修改父组件状态
  4. 纯函数理念:React 组件应像纯函数一样,相同的 props 输入总是产生相同的输出

面试题 4:子组件如何修改父组件的数据?

参考答案:

子组件不能直接修改父组件的数据,但可以通过回调函数的方式通知父组件修改:

  1. 父组件定义一个修改状态的函数
  2. 将这个函数作为 prop 传递给子组件
  3. 子组件在需要时调用这个函数,并传入参数
  4. 父组件的函数执行,更新自身 state
  5. state 变化后通过 props 重新传给子组件,完成更新闭环

面试题 5:什么是 Prop Drilling?如何解决?

参考答案:

Prop Drilling(属性钻探)是指数据需要通过多层中间组件层层传递才能到达目标组件,而中间组件并不使用这些数据,只是转发。

解决方案:

  1. 组件组合(Composition):将已经组装好的组件作为 children 传入,中间层不感知数据
  2. Context API:适合主题、语言、用户信息等跨层级共享的数据
  3. 状态管理库:Redux、Zustand 等,适合复杂的全局状态管理
  4. 合理拆分组件:避免过深的组件嵌套

面试题 6:React.memo 有什么用?它的原理是什么?

参考答案:

React.memo 是一个高阶组件,用于优化函数组件的性能。它会对组件的 props 进行浅比较,如果 props 没有变化,就跳过组件的重新渲染。

原理:

  • React.memo 包裹组件后,会缓存上一次的渲染结果
  • 每次父组件重渲染时,对新 props 和旧 props 做浅比较(只比较第一层引用)
  • 如果相同,直接复用缓存的渲染结果,不执行组件函数
  • 如果不同,重新渲染组件

注意事项:

  • 如果 props 中包含对象或函数,每次渲染都是新引用,React.memo 会失效
  • 需要配合 useMemo 和 useCallback 使用才能达到最佳效果
  • React.memo 不是万能的,过度使用反而增加比较开销

面试题 7:defaultProps 和参数默认值有什么区别?

参考答案:

  • defaultProps 是类组件时代的写法,在 React 19 中已从函数组件中移除
  • 参数默认值是 ES6 语法,更简洁,是现在的推荐写法
  • 功能上基本一致,都是为 props 提供默认值
javascript 复制代码
// 旧写法
function Child({ name }) {
  return <div>{name}</div>;
}
Child.defaultProps = { name: '默认' };

// 新写法(推荐)
function Child({ name = '默认' }) {
  return <div>{name}</div>;
}

面试题 8:children 是什么?有哪些用法?

参考答案:

children 是一个特殊的 prop,代表组件开始标签和结束标签之间的内容。

用法:

  1. 普通内容:文本、JSX 元素
  2. 函数(Render Props):传递一个函数,实现代码复用
  3. React.Children 工具 :React.Children.map、React.Children.toArray 等操作 children
  4. 具名插槽替代:通过多个 prop 传递不同位置的内容

面试题 9:如何对 Props 进行类型检查?

参考答案:

  1. TypeScript(推荐):在企业项目中最常用,编译时检查,支持复杂类型
  2. PropTypes:运行时检查,适合 JavaScript 项目,React 19 中函数组件已移除支持
  3. 默认值兜底:配合参数默认值,减少 undefined 导致的错误

面试题 10:React 19 对 Props 做了哪些改变?

参考答案:

  1. ref 成为普通 prop :函数组件可以直接从 props 中解构 ref,不再需要 forwardRef
  2. 移除函数组件的 defaultProps:推荐使用参数默认值
  3. 移除函数组件的 propTypes:推荐使用 TypeScript
  4. 自定义元素支持:props 可以更好地传递给 Web Components

十、总结与最佳实践清单

10.1 核心要点回顾

  1. Props 是 React 组件通信的基石,掌握它是学好 React 的第一步
  2. 单向数据流 + 只读性 是 Props 的灵魂,理解了这两点就理解了 Props 的设计哲学
  3. 子传父用回调函数,这是 React 的标准模式
  4. Prop Drilling 用组合模式解决,不要一上来就用 Context 或 Redux
  5. 性能优化靠 React.memo + useMemo + useCallback,但不要过度优化

10.2 企业级最佳实践清单

  • 使用函数组件 + 解构赋值 + 参数默认值
  • 使用 TypeScript 定义 Props 类型,必填项不加 ?
  • 容器组件负责逻辑,展示组件只负责渲染
  • 回调函数命名用 onXxx 格式,如 onClick、onSubmit
  • 避免在渲染中创建新对象和新函数作为 props
  • 纯展示组件用 React.memo 包裹
  • 优先用组合模式解决 Prop Drilling,而不是 Context
  • 不要在子组件中直接修改 props 或 props 中的对象属性
  • 列表渲染必须加 key,且不要用 index 作为 key
  • 组件 Props 接口保持稳定,避免频繁变更

10.3 学习路径建议

  1. 入门阶段:掌握基础传递、解构、默认值、回调函数
  2. 进阶阶段:理解单向数据流、学习组合模式、Render Props
  3. 高级阶段:性能优化、TypeScript 类型体操、设计模式
  4. 架构阶段:组件设计原则、状态管理选型、跨团队组件库建设

Props 看似简单,但它是 React 整个组件化体系的根基。把 Props 用好了,你的 React 代码会更清晰、更易维护、性能更好。希望这篇文章能帮你彻底掌握 Props,在日常开发和面试中都能游刃有余。


转载声明:本文为原创文章,如需转载,请联系作者获得授权,并注明出处。

相关推荐
Flynt1 小时前
开源"间谍卫星"说数据全是真的,我把它的免key数据源挨个试了一遍
前端·开源·数据可视化
Csvn2 小时前
渲染管线
前端
程序员大阳2 小时前
副队长CSS教程(33)–圆角与透明
前端·css·透明·圆角
志尊宝2 小时前
Vue3 零基础每日笔记(082):文件上传实战——el-upload 校验、自定义上传、头像预览
前端·javascript·vue.js·笔记·#javascript·#vue.js
鬼手点金3 小时前
opencode-rules 插件
java·开发语言·前端·人工智能·chrome·git·github
柳杉3 小时前
借助 Codex 和可视化工作流,从调研到完整实现,3D 大屏竟然可以这么简单
前端·ai编程·数据可视化
IMPYLH3 小时前
HTML 的 <wbr> 元素
前端·html
Dovis(誓平步青云)4 小时前
常用命令藏得太深?怎么做一个快捷命令坞
运维·前端·自动化·音视频·prometheus
IT_陈寒4 小时前
Vite的这个问题坑了我三天,原来如此解决!
前端·人工智能·后端