陌上人如玉
公子世无双

1.3 Props

1.3 Props

一句话:Props 是父组件传给子组件的只读数据,它是「单向数据流」的载体——子组件不能修改 props,只能通过调用父组件传下来的回调来请求变更。

一、Props 的本质

// 组件接收 props 就是一个对象
function UserCard(props) {
  console.log(props);     // { name: '张三', age: 18, children: [...] }
  return 
{props.name}
; } // 用解构更清晰 function UserCard({ name, age, children }) { return (

{name}

{age} {children}
); } // 使用(JSX 属性会组成 props 对象) 内容
JSX 与编译产物的对应关系:

内容
   ↓
jsx(UserCard, { name: '张三', age: 18, children: '内容' })
   ↓
UserCard({ name: '张三', age: 18, children: '内容' })
↑ 组件本质上就是「用 props 对象调用的函数」

二、为什么 Props 是只读的

// ❌ 不能修改 props
function Child({ user }) {
  user.name = '李四';            // 违反约定(而且对基础类型根本无效)
  return 
{user.name}
; } // ✅ 需要改数据时:通过回调请求父组件更新 function Child({ user, onChange }) { return ; } function Parent() { const [user, setUser] = useState({ name: '张三' }); return setUser({ ...user, name })} />; }

为什么必须只读(三个理由):

理由 说明
数据流向可预测 数据只从父到子,任何一个值的来源唯一,便于调试
避免状态不同步 如果子组件能改 props,父组件的 state 与子组件的显示会不一致
让更新可追踪 状态变更集中在「拥有该状态的组件」里,容易排查问题

注意一个反直觉的点:如果 props 是对象或数组,在子组件里修改它的属性(user.name = 'x')在技术上是「生效」的(因为传递的是引用),React 也不会报错。但这破坏了单向数据流,是明确的错误做法——因为父组件不知道数据被改了,下次渲染又会被覆盖,行为难以预测。

三、默认值

// ✅ 函数组件:用「默认参数」(推荐)
function Greeting({ name = '访客', age = 0 }) {
  return 
{name} {age}
; } // ⚠️ 对象/数组作为默认值时,注意「每次渲染都是新对象」 function List({ items = [], onSelect = () => {} }) { // ⚠️ 默认的 [] 与 () => {} 每次渲染都是新引用 // → 如果有依赖它们的 useEffect / useMemo,会每次触发 } // ✅ 更稳妥:把默认值提到模块作用域 const EMPTY_ARRAY = []; const noop = () => {}; function List2({ items = EMPTY_ARRAY, onSelect = noop }) { // 引用稳定 ✓ return null; }
// ❌ defaultProps 已被废弃(函数组件上会警告,React 19 移除)
Greeting.defaultProps = { name: '访客' };

// ✅ 用默认参数代替
function Greeting2({ name = '访客' }) {}
写法 状态 建议
默认参数 ({ name = 'x' }) ✅ 推荐 函数组件标准写法
defaultProps ❌ 已废弃(React 18 警告、19 移除) 不要用
undefined 触发默认值 ✅ 传 null 不会触发默认值 ⚠️
function Comp({ value = 'default' }) {
  return 
{value}
; } {/* 'default' */} {/* 'default'(undefined 触发默认值) */} {/* 渲染空(null 不触发默认值)⚠️ */} {/* 空字符串(不触发默认值) */}

四、children 的几种形态

// ① 单个元素
内容

// children:

内容 // ② 多个元素

标题

内容

// children: [

,

] // ③ 纯文本 纯文本 // children: '纯文本' // ④ 函数(渲染函数 / render props) {(data) =>

{data}
} // children: (data) =>
{data}
// ⑤ 什么都不传 // children: undefined

// tips: children 的类型
type Props = {
  children: React.ReactNode;      // ⭐ 最常用:任何可渲染的内容
};
type Props2 = {
  children: React.ReactElement;   // 只允许单个元素(不允许文本/数组)
};
type Props3 = {
  children: (data: Data) => React.ReactNode;   // 渲染函数
};

// React.ReactNode 的定义(简化)
type ReactNode =
  | ReactElement
  | string
  | number
  | boolean
  | null
  | undefined
  | Iterable
  | ReactPortal;

五、展开传递与透传

// ① 展开传递(把对象的属性一次性传给子组件)
const user = { name: '张三', age: 18, avatar: 'a.png' };
           // 等价于 name={user.name} age={user.age} avatar={user.avatar}

// ② 有选择地传递
const { password, ...safe } = user;      // 剔除敏感字段再传


// ③ 覆盖(后面的生效)
        // name 变成「李四」

// ④ 透传其余 props(组件库常见模式)
function Input({ label, ...rest }: InputProps) {
  return (
    
  );
}
// ⑤ 展开传递与 key 的关系
const list = [{ id: 1, name: 'a' }];
{list.map((item) => )}
// ⚠️ 如果 item 里也有 key 属性,会被后面的 {...item} 覆盖
// ✅ 把 key 放在最后,或确保数据里没有同名字段
{list.map((item) => )}

六、多个「插槽」的写法(React 没有 slot)

// React 用 props 传元素来实现「具名插槽」
interface LayoutProps {
  header: React.ReactNode;
  sidebar?: React.ReactNode;
  children: React.ReactNode;
  footer?: React.ReactNode;
}

function Layout({ header, sidebar, children, footer }: LayoutProps) {
  return (
    
{header}
{sidebar && }
{children}
{footer &&
{footer}
}
); } // 使用(比 Vue 的