文章目录
- 前言
- **React 中更高级的 props 设计模式**,
- 🎯 高级 Props 模式:Render Props、asChild、Slot 模式、函数式 children、Context 替代
前言
在 React 中,props
是组件之间通信的核心机制之一。虽然 props
看起来简单(像参数一样传入组件),但它实际上有许多关键细节和机制,掌握这些对于写出高效、稳定、易维护的 React 组件非常重要。
✅ 一、什么是 props?
props
(全称 properties)是组件的“属性”,用于从父组件向子组件传递数据。
function Hello({ name }) {
return <h1>Hello, {name}!</h1>
}
// 使用:
<Hello name="Tom" />
✅ 二、props 的特点
特性 | 说明 |
---|---|
📦 只读 | 子组件不能直接修改 props |
⛓️ 单向数据流 | 数据只能从父组件流向子组件 |
🔁 每次 render 都会重新传入 | props 是受控的,与状态更新同步变化 |
🧱 支持任意类型 | 可以传入字符串、数字、对象、函数、JSX、组件 |
✅ 三、props 的核心细节 & 常见问题
1. props 是新对象还是引用?
- 每次父组件 render,子组件都会收到新的
props
对象引用(即使值不变)。 - 所以你应该使用
React.memo
来优化子组件避免不必要的重渲染。
const MemoChild = React.memo(({ data }) => { ... })
⚠️ 注意:传入的是对象或函数时,
===
变了就会触发子组件 render。
2. 函数作为 props:闭包陷阱
<Child onClick={() => doSomething()} />
这种写法会在每次父组件渲染时创建新的函数,导致子组件无法 memo 优化。解决方法:
const memoized = useCallback(() => doSomething(), [])
<Child onClick={memoized} />
3. 默认值 & 解构默认值
function Greeting({ name = "Guest" }) {
return <h1>Hello, {name}</h1>
}
或者使用默认 props(对于类组件):
MyComponent.defaultProps = {
name: 'Guest'
}
⚠️ 对于函数组件推荐解构默认值。
4. props.children 是什么?
props.children
是组件标签内嵌套的内容:
<Panel>
<p>This is child</p>
</Panel>
function Panel({ children }) {
return <div>{children}</div>
}
children
可以是字符串、JSX、数组、函数等。
5. props + JSX 组合:可传组件、函数、Slot
<MyButton renderIcon={() => <Icon />} />
允许传递函数或组件作为 props,实现类似插槽功能。
🧠 四、最佳实践
场景 | 建议 |
---|---|
子组件依赖复杂 props 变化 | 用 React.memo + useCallback 优化 |
多个 props 逻辑交叉 | 抽象成对象 props:如 config , options |
子组件行为复杂 | 用 props 传入回调而不是在组件内部硬编码 |
渲染内容灵活 | 使用 props.children 或 render prop 模式 |
✅ 五、TS 中定义 props
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
如何与context
和state
配合?- 如何实现动态 Slot(类似 Vue 的具名插槽)?
- 或如何设计组件库的高级 props 模式(如 render props、asChild 模式)?
React 中更高级的 props 设计模式,
尤其适用于你在封装可复用组件库时的实践需求,包括:
🎯 高级 Props 模式:Render Props、asChild、Slot 模式、函数式 children、Context 替代
✅ 一、Render Props 模式
通过 props 传入一个函数,让子组件决定如何渲染内容。
示例:状态控制组件
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
决定渲染为哪个标签
<Button asChild>
<Link href="/about">Go to About</Link>
</Button>
Button 实现:
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
更自然(常用于列表渲染、状态暴露):
<MyList>
{(item) => <div key={item.id}>{item.name}</div>}
</MyList>
类似于 Vue 中的具名插槽默认插槽:
<template #default="slotProps">...</template>
✅ 四、Slot 模式(具名插槽模拟)
React 没有官方 Slot,但可以模拟:
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 替代
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 prop 或 children 是函数 |
保留标签结构灵活性 | 使用 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 实现可访问性、动画、组合组件设计
📦 安装依赖(如未安装)
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
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
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
自定义触发器样式
<Modal
asChild
trigger={<a className="underline cursor-pointer">点我打开</a>}
...
>
✅ 总结功能支持
功能 | 实现方式 |
---|---|
具名插槽 Header/Body/Footer | 使用 title 、description 、children 、footer props |
自定义触发按钮 | trigger + asChild |
支持嵌套组件内容 | children 支持 JSX |
可访问性与焦点管理 | 使用 shadcn/ui Dialog 组件封装 |
后面可以扩展这个模态框支持:
- ✅ 异步加载内容(Skeleton)
- ✅ 表单提交 loading 状态
- ✅ 动态打开多个 Modal(如嵌套或全局注册)
- ✅ 在服务端渲染中与 RSC 配合