一、最基础的:children prop(默认插槽)
基本用法
javascript
// 父组件
function App() {
return (
<Card>
<h2>这是标题</h2>
<p>这是内容...</p>
<button>点击</button>
</Card>
);
}
// 子组件 Card(类似 Vue 的默认插槽)
function Card({ children }) {
return (
<div className="card">
{children} {/* 这里就是插槽位置 */}
</div>
);
}
处理没有 children 的情况
jsx
javascript
function Card({ children }) {
return (
<div className="card">
{children || <p>默认内容</p>}
</div>
);
}
二、命名插槽(类似 Vue 的具名插槽)
方法1:使用多个 props
javascript
// 子组件:定义多个插槽位置
function Layout({ header, sidebar, content, footer }) {
return (
<div className="layout">
<header className="header">{header}</header>
<aside className="sidebar">{sidebar}</aside>
<main className="content">{content}</main>
<footer className="footer">{footer}</footer>
</div>
);
}
// 父组件使用
function App() {
return (
<Layout
header={<h1>网站标题</h1>}
sidebar={
<nav>
<a href="/">首页</a>
<a href="/about">关于</a>
</nav>
}
content={<p>主要内容区域...</p>}
footer={<p>版权信息 © 2024</p>}
/>
);
}
方法2:使用 children 对象(更接近 Vue 语法)
javascript
// 子组件
function Card({ children }) {
// children 可以是对象
return (
<div className="card">
<div className="card-header">
{children.header || <h3>默认标题</h3>}
</div>
<div className="card-body">
{children.body || <p>默认内容</p>}
</div>
<div className="card-footer">
{children.footer}
</div>
</div>
);
}
// 父组件
function App() {
return (
<Card>
{{
header: <h2>自定义标题</h2>,
body: <p>自定义内容...</p>,
footer: <button>确认</button>
}}
</Card>
);
}
三、作用域插槽(带数据的插槽)
方法1:使用 render props
javascript
// 子组件:提供数据给插槽
function DataList({ data, children }) {
return (
<div className="list">
{data.map((item, index) => (
// 调用 children 函数,传递数据
<div key={item.id}>
{children(item, index)}
</div>
))}
</div>
);
}
// 父组件使用
function App() {
const users = [
{ id: 1, name: '张三', age: 25 },
{ id: 2, name: '李四', age: 30 }
];
return (
<DataList data={users}>
{(user, index) => ( // 这里接收子组件传递的数据
<div className="user-item">
<span>{index + 1}. </span>
<strong>{user.name}</strong>
<span> ({user.age}岁)</span>
</div>
)}
</DataList>
);
}
方法2:使用函数作为 children
javascript
// 子组件
function Toggle({ children }) {
const [isOn, setIsOn] = useState(false);
const toggle = () => setIsOn(!isOn);
return children({
isOn,
toggle,
onText: '开启',
offText: '关闭'
});
}
// 父组件
function App() {
return (
<Toggle>
{({ isOn, toggle, onText, offText }) => (
<button onClick={toggle}>
{isOn ? onText : offText}
</button>
)}
</Toggle>
);
}
四、组合组件模式(类似 Web Components 的 <slot>)
方法1:使用特殊的子组件
javascript
// 定义插槽组件
const CardHeader = ({ children }) => children;
const CardBody = ({ children }) => children;
const CardFooter = ({ children }) => children;
// 容器组件
function Card({ children }) {
// 从 children 中提取不同插槽的内容
let header, body, footer;
React.Children.forEach(children, child => {
if (child.type === CardHeader) {
header = child.props.children;
} else if (child.type === CardBody) {
body = child.props.children;
} else if (child.type === CardFooter) {
footer = child.props.children;
}
});
return (
<div className="card">
{header && <div className="card-header">{header}</div>}
{body && <div className="card-body">{body}</div>}
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
// 父组件使用(类似 Vue 的语法)
function App() {
return (
<Card>
<CardHeader>
<h2>用户信息</h2>
</CardHeader>
<CardBody>
<p>姓名:张三</p>
<p>年龄:25</p>
</CardBody>
<CardFooter>
<button>编辑</button>
</CardFooter>
</Card>
);
}
方法2:使用 context + 特殊组件
jsx
ini
// 更高级的实现
const CardContext = React.createContext({});
const Card = ({ children }) => {
const [slots, setSlots] = useState({});
return (
<CardContext.Provider value={{ registerSlot: setSlots }}>
{children}
<div className="card">
<div className="card-header">{slots.header}</div>
<div className="card-body">{slots.body}</div>
<div className="card-footer">{slots.footer}</div>
</div>
</CardContext.Provider>
);
};
const CardHeader = ({ children }) => {
const { registerSlot } = useContext(CardContext);
useEffect(() => {
registerSlot(prev => ({ ...prev, header: children }));
}, [children, registerSlot]);
return null; // 不渲染自身
};
五、使用第三方库实现插槽
1. React Slot(专门库)
npm install react-slot
javascript
import { Slot, Fill } from 'react-slot';
function Layout() {
return (
<div>
<header>
<Slot name="header" />
</header>
<main>
<Slot name="content" />
</main>
</div>
);
}
function App() {
return (
<Layout>
<Fill name="header">
<h1>我的网站</h1>
</Fill>
<Fill name="content">
<p>欢迎光临</p>
</Fill>
</Layout>
);
}
六、实战案例:实现一个完整的弹窗组件
javascript
// 弹窗组件(支持插槽)
function Modal({
isOpen,
onClose,
title,
children,
footer
}) {
if (!isOpen) return null;
return (
<div className="modal-overlay">
<div className="modal">
{/* 标题插槽 */}
<div className="modal-header">
<h3>{title}</h3>
<button onClick={onClose} className="close-btn">×</button>
</div>
{/* 默认插槽(主要内容) */}
<div className="modal-body">
{children}
</div>
{/* 底部插槽 */}
<div className="modal-footer">
{footer || ( // 默认底部
<button onClick={onClose}>关闭</button>
)}
</div>
</div>
</div>
);
}
// 使用示例
function App() {
const [showModal, setShowModal] = useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>打开弹窗</button>
<Modal
isOpen={showModal}
onClose={() => setShowModal(false)}
title="用户设置"
footer={ // 具名插槽
<>
<button onClick={() => setShowModal(false)}>取消</button>
<button onClick={() => alert('保存成功')}>保存</button>
</>
}
>
{/* 默认插槽内容 */}
<form>
<label>
用户名:
<input type="text" />
</label>
<label>
邮箱:
<input type="email" />
</label>
</form>
</Modal>
</div>
);
}
七、高级模式:动态插槽
动态决定插槽位置
javascript
function DynamicLayout({ children, layout = 'default' }) {
// 根据布局类型动态渲染插槽
const layouts = {
default: (
<div className="default-layout">
<div className="top">{children.top}</div>
<div className="main">{children.main}</div>
</div>
),
sidebar: (
<div className="sidebar-layout">
<aside>{children.sidebar}</aside>
<main>{children.main}</main>
</div>
),
grid: (
<div className="grid-layout">
{children.grid?.map((item, index) => (
<div key={index} className="grid-item">{item}</div>
))}
</div>
)
};
return layouts[layout];
}
// 使用
<DynamicLayout layout="sidebar">
{{
sidebar: <nav>导航菜单</nav>,
main: <article>主要内容</article>
}}
</DynamicLayout>
八、与 Vue 插槽的对比
| Vue 插槽 | React 对应实现 | 代码示例 |
|---|---|---|
<slot /> |
{children} |
<div>{children}</div> |
<slot name="header"> |
多个 props | header={content} |
| 作用域插槽 | render props | {(data) => <div>{data}</div>} |
<template #header> |
组件组合 | <Card><CardHeader>...</CardHeader></Card> |
九、最佳实践建议
何时使用哪种方式?
- 简单内容传递 →
childrenprop - 多个固定区域 → 多个 props(命名插槽)
- 需要向父组件传递数据 → render props
- 类似 Vue 的具名插槽语法 → 组件组合模式
- 复杂插槽逻辑 → Context + 特殊组件
性能考虑
javascript
// ❌ 避免每次渲染都创建新的插槽内容
<Card>
{{
header: <Header />, // 每次都是新组件
body: <Body />
}}
</Card>
// ✅ 使用 useMemo 或提取到组件外部
const cardContent = useMemo(() => ({
header: <Header />,
body: <Body />
}), [dependencies]);
<Card>{cardContent}</Card>
reaact 中的性能优化有很多需要注意的地方,欢迎大家说说平时遇到的情况