React-props

文章目录

  • 前言
    • [✅ 一、什么是 props?](#✅ 一、什么是 props?)
    • [✅ 二、props 的特点](#✅ 二、props 的特点)
    • [✅ 三、props 的核心细节 & 常见问题](#✅ 三、props 的核心细节 & 常见问题)
      • [1. **props 是新对象还是引用?**](#1. props 是新对象还是引用?)
      • [2. **函数作为 props:闭包陷阱**](#2. 函数作为 props:闭包陷阱)
      • [3. **默认值 & 解构默认值**](#3. 默认值 & 解构默认值)
      • [4. **props.children 是什么?**](#4. props.children 是什么?)
      • [5. **props + JSX 组合:可传组件、函数、Slot**](#5. props + JSX 组合:可传组件、函数、Slot)
    • [🧠 四、最佳实践](#🧠 四、最佳实践)
    • [✅ 五、TS 中定义 props](#✅ 五、TS 中定义 props)
    • [✅ 总结](#✅ 总结)
  • [**React 中更高级的 props 设计模式**,](#React 中更高级的 props 设计模式,)
  • [🎯 高级 Props 模式:Render Props、asChild、Slot 模式、函数式 children、Context 替代](#🎯 高级 Props 模式:Render Props、asChild、Slot 模式、函数式 children、Context 替代)
    • [✅ 一、Render Props 模式](#✅ 一、Render Props 模式)
    • [✅ 二、`asChild` 模式(shadcn/ui 的经典模式)](#✅ 二、asChild 模式(shadcn/ui 的经典模式))
      • [示例:Button 接收 `asChild` 决定渲染为哪个标签](#示例:Button 接收 asChild 决定渲染为哪个标签)
      • [Button 实现:](#Button 实现:)
      • [✅ 优点:](#✅ 优点:)
    • [✅ 三、函数式 children 模式(Function-as-Children)](#✅ 三、函数式 children 模式(Function-as-Children))
    • [✅ 四、Slot 模式(具名插槽模拟)](#✅ 四、Slot 模式(具名插槽模拟))
    • [✅ 五、Context 替代 Deep Props Drilling](#✅ 五、Context 替代 Deep Props Drilling)
    • [📚 实战建议(封装组件库时)](#📚 实战建议(封装组件库时))
    • [✅ 总结](#✅ 总结)
    • [📦 安装依赖(如未安装)](#📦 安装依赖(如未安装))
    • [🧱 组件结构目录建议:](#🧱 组件结构目录建议:)
    • [✅ 1. `components/ui/modal.tsx`](#✅ 1. components/ui/modal.tsx)
    • [✅ 2. 示例用法 `features/demo-modal.tsx`](#✅ 2. 示例用法 features/demo-modal.tsx)
    • [✅ 3. 使用 `asChild` 自定义触发器样式](#✅ 3. 使用 asChild 自定义触发器样式)
    • [✅ 总结功能支持](#✅ 总结功能支持)

前言

在 React 中,props 是组件之间通信的核心机制之一。虽然 props 看起来简单(像参数一样传入组件),但它实际上有许多关键细节和机制,掌握这些对于写出高效、稳定、易维护的 React 组件非常重要。


✅ 一、什么是 props?

props(全称 properties )是组件的"属性",用于从父组件向子组件传递数据

ts 复制代码
function Hello({ name }) {
  return <h1>Hello, {name}!</h1>
}

// 使用:
<Hello name="Tom" />

✅ 二、props 的特点

特性 说明
📦 只读 子组件不能直接修改 props
⛓️ 单向数据流 数据只能从父组件流向子组件
🔁 每次 render 都会重新传入 props 是受控的,与状态更新同步变化
🧱 支持任意类型 可以传入字符串、数字、对象、函数、JSX、组件

✅ 三、props 的核心细节 & 常见问题

1. props 是新对象还是引用?

  • 每次父组件 render,子组件都会收到新的 props 对象引用(即使值不变)。
  • 所以你应该使用 React.memo 来优化子组件避免不必要的重渲染。
ts 复制代码
const MemoChild = React.memo(({ data }) => { ... })

⚠️ 注意:传入的是对象或函数时,=== 变了就会触发子组件 render。


2. 函数作为 props:闭包陷阱

ts 复制代码
<Child onClick={() => doSomething()} />

这种写法会在每次父组件渲染时创建新的函数,导致子组件无法 memo 优化。解决方法:

ts 复制代码
const memoized = useCallback(() => doSomething(), [])
<Child onClick={memoized} />

3. 默认值 & 解构默认值

ts 复制代码
function Greeting({ name = "Guest" }) {
  return <h1>Hello, {name}</h1>
}

或者使用默认 props(对于类组件):

js 复制代码
MyComponent.defaultProps = {
  name: 'Guest'
}

⚠️ 对于函数组件推荐解构默认值。


4. props.children 是什么?

props.children组件标签内嵌套的内容

ts 复制代码
<Panel>
  <p>This is child</p>
</Panel>
ts 复制代码
function Panel({ children }) {
  return <div>{children}</div>
}

children 可以是字符串、JSX、数组、函数等。


5. props + JSX 组合:可传组件、函数、Slot

ts 复制代码
<MyButton renderIcon={() => <Icon />} />

允许传递函数或组件作为 props,实现类似插槽功能。


🧠 四、最佳实践

场景 建议
子组件依赖复杂 props 变化 React.memo + useCallback 优化
多个 props 逻辑交叉 抽象成对象 props:如 config, options
子组件行为复杂 用 props 传入回调而不是在组件内部硬编码
渲染内容灵活 使用 props.childrenrender prop 模式

✅ 五、TS 中定义 props

ts 复制代码
type MyProps = {
  name: string
  onClick?: () => void
  children?: React.ReactNode
}

const MyComponent: React.FC<MyProps> = ({ name, onClick, children }) => (
  <div>{name}</div>
)

✅ 总结

说明
只读 组件内部不能修改 props
单向 从父 → 子流动,反向用回调
优化 使用 memo、useCallback 避免重复渲染
TS 类型 用接口定义 props,增强代码可维护性
children 支持插槽、嵌套等灵活布局

如果你想了解:

  • props 如何与 contextstate 配合?
  • 如何实现动态 Slot(类似 Vue 的具名插槽)?
  • 或如何设计组件库的高级 props 模式(如 render props、asChild 模式)?

React 中更高级的 props 设计模式

尤其适用于你在封装可复用组件库时的实践需求,包括:


🎯 高级 Props 模式:Render Props、asChild、Slot 模式、函数式 children、Context 替代


✅ 一、Render Props 模式

通过 props 传入一个函数,让子组件决定如何渲染内容。

示例:状态控制组件

ts 复制代码
const Toggle = ({ children }: { children: (on: boolean, toggle: () => void) => React.ReactNode }) => {
  const [on, setOn] = useState(false)
  const toggle = () => setOn(!on)
  return <>{children(on, toggle)}</>
}

// 使用方式
<Toggle>
  {(on, toggle) => (
    <button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>
  )}
</Toggle>

✅ 优点:

  • 父组件控制状态
  • 子组件控制渲染方式(灵活解耦)

✅ 二、asChild 模式(shadcn/ui 的经典模式)

允许你将组件的行为注入到任意 HTML 元素或组件上。

示例:Button 接收 asChild 决定渲染为哪个标签

ts 复制代码
<Button asChild>
  <Link href="/about">Go to About</Link>
</Button>

Button 实现:

ts 复制代码
import { Slot } from "@radix-ui/react-slot"

const Button = ({ asChild = false, children, ...props }) => {
  const Comp = asChild ? Slot : 'button'
  return <Comp {...props} className="btn">{children}</Comp>
}

✅ 优点:

  • 完美兼容无障碍结构 <a> / <Link>
  • 支持组合式组件

✅ 三、函数式 children 模式(Function-as-Children)

render prop 更自然(常用于列表渲染、状态暴露):

ts 复制代码
<MyList>
  {(item) => <div key={item.id}>{item.name}</div>}
</MyList>

类似于 Vue 中的具名插槽默认插槽:

html 复制代码
<template #default="slotProps">...</template>

✅ 四、Slot 模式(具名插槽模拟)

React 没有官方 Slot,但可以模拟:

ts 复制代码
type PanelProps = {
  header: React.ReactNode
  footer: React.ReactNode
  children: React.ReactNode
}

const Panel = ({ header, footer, children }: PanelProps) => (
  <div className="panel">
    <div className="header">{header}</div>
    <div className="body">{children}</div>
    <div className="footer">{footer}</div>
  </div>
)

// 使用:
<Panel
  header={<h2>Title</h2>}
  footer={<button>Submit</button>}
>
  <p>Content</p>
</Panel>

✅ 五、Context 替代 Deep Props Drilling

当 props 需要一层一层传下去时,可用 Context 替代

ts 复制代码
const ThemeContext = createContext("light")

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Layout />
    </ThemeContext.Provider>
  )
}

function Layout() {
  return <Header />
}

function Header() {
  const theme = useContext(ThemeContext)
  return <h1 className={theme}>Theme Header</h1>
}

📚 实战建议(封装组件库时)

目标 推荐做法
自定义渲染内容 使用 render propchildren 是函数
保留标签结构灵活性 使用 asChild + Slot
复杂嵌套内容 用具名插槽模式(多个 props 渲染区域)
深度通信 用 React Context 提供状态
封装 + 组合 使用 Compound Components 模式(Header.Body.Footer)

✅ 总结

模式 用途 类比 Vue
render prop 内容渲染完全交给父组件 <slot :data="...">
asChild 自定义渲染元素 <component :is="...">
props.children 默认内容插槽 <slot />
Context 深层组件通信 provide/inject
Slot 模拟 多区域结构控制 具名 <slot name="...">

下面是一个完整封装的 Modal(模态框)组件示例,结合了:

  • asChild 模式(支持自定义触发按钮)
  • ✅ Slot 模式(支持具名插槽:Header、Body、Footer)
  • shadcn/ui + Radix UI 实现可访问性、动画、组合组件设计

📦 安装依赖(如未安装)

bash 复制代码
npx shadcn-ui@latest add dialog button

🧱 组件结构目录建议:

复制代码
components/
  ui/
    modal.tsx        ← 模态框主组件封装(含 asChild + slot 模式)
  features/
    demo-modal.tsx   ← 示例使用:组合 Header/Body/Footer + 触发按钮

✅ 1. components/ui/modal.tsx

ts 复制代码
import * as React from "react"
import {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogClose,
} from "@/components/ui/dialog"
import { Slot } from "@radix-ui/react-slot"

interface ModalProps {
  trigger?: React.ReactNode
  title?: React.ReactNode
  description?: React.ReactNode
  footer?: React.ReactNode
  children: React.ReactNode
  asChild?: boolean
}

export function Modal({
  trigger,
  title,
  description,
  footer,
  children,
  asChild = false,
}: ModalProps) {
  return (
    <Dialog>
      {trigger && (
        <DialogTrigger asChild={asChild}>
          {trigger}
        </DialogTrigger>
      )}
      <DialogContent>
        {(title || description) && (
          <DialogHeader>
            {title && <DialogTitle>{title}</DialogTitle>}
            {description && <DialogDescription>{description}</DialogDescription>}
          </DialogHeader>
        )}
        <div className="py-2">{children}</div>
        {footer && (
          <DialogFooter>
            {footer}
          </DialogFooter>
        )}
      </DialogContent>
    </Dialog>
  )
}

✅ 2. 示例用法 features/demo-modal.tsx

ts 复制代码
import { Modal } from "@/components/ui/modal"
import { Button } from "@/components/ui/button"

export default function DemoModal() {
  return (
    <Modal
      trigger={<Button>打开模态框</Button>}
      title="模态框标题"
      description="这是模态框描述"
      footer={
        <>
          <DialogClose asChild>
            <Button variant="outline">取消</Button>
          </DialogClose>
          <Button>确认</Button>
        </>
      }
    >
      <p>你可以在这里放任何自定义内容,如表单、提示、嵌套组件等。</p>
    </Modal>
  )
}

✅ 3. 使用 asChild 自定义触发器样式

ts 复制代码
<Modal
  asChild
  trigger={<a className="underline cursor-pointer">点我打开</a>}
  ...
>

✅ 总结功能支持

功能 实现方式
具名插槽 Header/Body/Footer 使用 titledescriptionchildrenfooter props
自定义触发按钮 trigger + asChild
支持嵌套组件内容 children 支持 JSX
可访问性与焦点管理 使用 shadcn/ui Dialog 组件封装

后面可以扩展这个模态框支持:

  • ✅ 异步加载内容(Skeleton)
  • ✅ 表单提交 loading 状态
  • ✅ 动态打开多个 Modal(如嵌套或全局注册)
  • ✅ 在服务端渲染中与 RSC 配合
相关推荐
江城开朗的豌豆15 分钟前
JavaScript篇:如何实现add(1)(2)(3)()=6?揭秘链式调用的终极奥义!
前端·javascript·面试
江城开朗的豌豆21 分钟前
JavaScript篇:GET、POST、PUT...傻傻分不清?一篇文章带你玩转HTTP请求!
前端·javascript·面试
恋猫de小郭2 小时前
Flutter 官方多窗口体验 ,为什么 Flutter 推进那么慢,而 CMP 却支持那么快
android·前端·flutter
云边有个稻草人3 小时前
智启未来:当知识库遇见莫奈的调色盘——API工作流重构企业服务美学
前端·数据库
仟濹8 小时前
【HTML】基础学习【数据分析全栈攻略:爬虫+处理+可视化+报告】
大数据·前端·爬虫·数据挖掘·数据分析·html
小小小小宇9 小时前
前端WebWorker笔记总结
前端
小小小小宇9 小时前
前端监控用户停留时长
前端
小小小小宇9 小时前
前端性能监控笔记
前端
烛阴10 小时前
Date-fns教程:现代JavaScript日期处理从入门到精通
前端·javascript
全栈小510 小时前
【前端】Vue3+elementui+ts,TypeScript Promise<string>转string错误解析,习惯性请出DeepSeek来解答
前端·elementui·typescript·vue3·同步异步