React(六)Props 里的 children:React 19 中的“卡槽”全解
目标:把 React 19 里
children这个“卡槽”讲透。读完这篇文章,你应该能清楚:
- 卡槽到底是什么?
- 有哪些写法?
- TypeScript 里怎么写类型?
- 哪些坑最容易踩?
1. 一句话定义:什么是“卡槽”?
在 React 里,卡槽就是 children prop。
你可以把它想象成现实生活中的“插槽”:
- 一个组件定义了一个“外壳”(比如卡片、布局、对话框)。
- 这个外壳留出一个或多个“卡槽”,让外部决定要插什么内容进去。
- 在 React 里,这个卡槽就是用
<组件>内容</组件>中间的内容表示的,它会被自动打包成props.children传给组件。
// Card 组件就是一个外壳,它里面留出 children 卡槽
function Card({ children }) {
return <div className="card">{children}</div>;
}
// 使用的时候,中间的内容就是插进卡槽里的东西
<Card>
<h2>我是插进来的标题</h2>
<p>我是插进来的正文</p>
</Card>;
在 React 19 中,这个机制没有任何本质变化,只是 TypeScript 类型和 Server Components 的组合方式上有了更细致的规范。下面会详细展开。
2. children 到底是什么类型?
2.1 运行时的类型
children 本质上就是一个 prop。它只不过是一个“名字被 React 保留、位置特殊”的 prop:放在开始标签和结束标签之间。
运行时,它可以是下面这些东西中的任意一种:
| 值 | 说明 |
|---|---|
string |
普通文本节点 |
number |
数字,会被渲染成文本 |
boolean / null / undefined |
不渲染任何东西 |
ReactElement |
单个 JSX 元素,比如 <div /> |
ReactElement[] |
多个 JSX 元素组成的数组 |
ReactPortal |
portals 内容 |
ReactFragment |
比如 <></> 包裹的内容 |
React 官方把所有这些可以合法出现在 JSX 里的东西统称为 ReactNode。
2.2 TypeScript 中的类型
在 React 19 中,推荐这样写类型:
import React from "react";
// 最常见的写法:children 可以是任何合法的 JSX 内容
function Card({ children }: { children: React.ReactNode }) {
return <div className="card">{children}</div>;
}
React 19 也提供了 React.PropsWithChildren 工具类型,让写法更简洁:
import React from "react";
interface CardProps {
title: string;
}
// 自动在 CardProps 上追加 children?: React.ReactNode
function Card({ title, children }: React.PropsWithChildren<CardProps>) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}
注意:React 19 中
JSX.Element这个类型建议写作React.JSX.Element,因为它已经被迁移到 React 命名空间下。老项目里的JSX.Element大部分情况下还能工作,但新项目建议用React.JSX.Element。
3. 基础用法:四种常见的卡槽写法
3.1 默认卡槽:组件中间的内容
这是最常见的用法,也就是把内容放在组件标签之间。
// 组件定义
function Card({ children }: { children: React.ReactNode }) {
return <div className="card">{children}</div>;
}
// 组件使用
function App() {
return (
<Card>
<h2>今天吃什么?</h2>
<p>来一碗牛肉面吧。</p>
</Card>
);
}
优点:使用组件的人可以自由决定中间放什么,组件本身只负责外壳样式。
3.2 命名卡槽:把多个卡槽做成多个 props
有时候一个组件需要多个不同位置的卡槽,比如一个布局页面需要头部、侧边栏、主体、底部。
这种情况下,不要只依赖一个 children,而是给每个卡槽起一个名字作为 props。
interface LayoutProps {
header: React.ReactNode;
sidebar: React.ReactNode;
children: React.ReactNode;
footer?: React.ReactNode;
}
function Layout({ header, sidebar, children, footer }: LayoutProps) {
return (
<div className="layout">
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
{footer && <footer>{footer}</footer>}
</div>
);
}
// 使用
function App() {
return (
<Layout
header={<h1>后台管理系统</h1>}
sidebar={
<ul>
<li>首页</li>
<li>设置</li>
</ul>
}
footer={<p>版权所有 © 2026</p>}
>
<p>这里是主内容区域</p>
</Layout>
);
}
这种写法比 Vue 的具名插槽更直观:在 React 里,一个卡槽就是一个 prop,直接用 JSX 传值即可。
3.3 函数卡槽:children 是一个函数(Render Props)
有时候父组件不仅想“放内容进去”,还想“让子组件决定怎么渲染”。
这种写法叫 Render Props,本质上就是 children 是一个函数。
interface MouseTrackerProps {
children: (state: { x: number; y: number }) => React.ReactNode;
}
function MouseTracker({ children }: MouseTrackerProps) {
const [position, setPosition] = React.useState({ x: 0, y: 0 });
return (
<div
className="mouse-tracker"
onMouseMove={(e) => setPosition({ x: e.clientX, y: e.clientY })}
>
{children(position)}
</div>
);
}
// 使用
function App() {
return (
<MouseTracker>
{({ x, y }) => (
<p>
鼠标位置:x={x}, y={y}
</p>
)}
</MouseTracker>
);
}
关键点:
- 这里
children不是 JSX,而是一个函数。 - 组件内部通过
children(数据)来调用这个函数,得到真正要渲染的节点。 - 在 React 19 中依然完全支持,且 TypeScript 类型推断会更严格。
3.4 默认内容:没有插卡槽时显示兜底内容
有时候卡槽里用户没传内容,我们希望显示一个默认的提示。
function EmptyFallback({ children }: { children?: React.ReactNode }) {
return (
<div className="box">
{children ?? <p>这里暂时没有内容~</p>}
</div>
);
}
// 使用
<EmptyFallback>
<p>有内容</p>
</EmptyFallback>
<EmptyFallback />
// 没传 children,会显示默认提示
这里用
??(空值合并运算符)而不是||,因为||会把0、false这些合法值也误判为空。
4. React.Children:处理 children 的工具箱
children 不一定是个数组。它可能是单个元素、字符串、null、数组、Fragment,等等。
因此 React 提供了一个专门处理 children 的工具集:React.Children。
4.1 React.Children.map
给每个 child 做处理,并返回新的 children。
import React from "react";
function ListWithIcon({ children }: { children: React.ReactNode }) {
return (
<ul>
{React.Children.map(children, (child, index) => (
<li key={index}>
<span>⭐</span>
{child}
</li>
))}
</ul>
);
}
// 使用
<ListWithIcon>
<span>苹果</span>
<span>香蕉</span>
<span>橙子</span>
</ListWithIcon>;
注意:即使
children只有一个元素,React.Children.map也能正确处理,它会把单个元素当成只有一个元素的数组处理。
4.2 React.Children.forEach
和 map 类似,但不返回新的数组,只用于遍历。
function logChildren(children: React.ReactNode) {
React.Children.forEach(children, (child, index) => {
console.log(`第 ${index} 个子节点:`, child);
});
}
4.3 React.Children.count
返回子节点的数量,包括 null 和 undefined 都会被计算进去。
function InfoPanel({ children }: { children: React.ReactNode }) {
const count = React.Children.count(children);
return (
<div>
<p>当前有 {count} 个子项</p>
{children}
</div>
);
}
4.4 React.Children.toArray
把 children 转换成一个扁平数组,并自动处理 Fragment、null、数组嵌套等情况。
function ReversedList({ children }: { children: React.ReactNode }) {
const items = React.Children.toArray(children);
return <div>{items.reverse()}</div>;
}
注意:
React.Children.toArray返回的数组里的每个元素都会被加上key,所以不要直接修改这个数组,否则可能破坏 React 的 diff 逻辑。
4.5 React.Children.only
确保 children 只有一个元素,否则抛出错误。
function SingleChildSlot({ children }: { children: React.ReactNode }) {
const onlyChild = React.Children.only(children);
return <div className="single-slot">{onlyChild}</div>;
}
// 正确
<SingleChildSlot>
<p>唯一的内容</p>
</SingleChildSlot>
// 错误:会报错
<SingleChildSlot>
<p>第一个</p>
<p>第二个</p>
</SingleChildSlot>
5. 实战:一个完整的“多卡槽”组件
下面是一个教学场景里常用的完整示例:一个 Dialog 对话框,支持标题卡槽、内容卡槽、底部按钮卡槽。
import React from "react";
interface DialogProps {
title?: React.ReactNode; // 命名卡槽:标题
footer?: React.ReactNode; // 命名卡槽:底部按钮
children: React.ReactNode; // 默认卡槽:正文
}
function Dialog({
title,
children,
footer,
}: React.PropsWithChildren<DialogProps>) {
return (
<div className="dialog-backdrop">
<div className="dialog">
{title && <div className="dialog-header">{title}</div>}
<div className="dialog-body">{children}</div>
{footer && <div className="dialog-footer">{footer}</div>}
</div>
</div>
);
}
// 使用
function App() {
return (
<Dialog
title={<h2>确认删除?</h2>}
footer={
<>
<button>取消</button>
<button>确认</button>
</>
}
>
<p>删除后数据将无法恢复,请谨慎操作。</p>
</Dialog>
);
}
这个例子展示了:
children作为默认卡槽。title和footer作为命名卡槽。- 用
React.ReactNode让卡槽类型最灵活。
6. React 19 中值得注意的变化和细节
6.1 JSX 类型命名空间迁移
React 19 中,JSX 的类型现在统一放在 React.JSX 命名空间下:
// 老写法(React 18 及以前常用)
function Old({ children }: { children: JSX.Element }) {
return <div>{children}</div>;
}
// React 19 推荐写法
function New({ children }: { children: React.JSX.Element }) {
return <div>{children}</div>;
}
不过日常教学和普通项目里,最推荐的还是用 React.ReactNode,因为它涵盖最全。
6.2 Server Components 和 children 卡槽
React 19 对 React Server Components(RSC)的支持更完善了。一个重要的细节:
Server Component 可以作为 children 传给 Client Component。
// ServerComponent.tsx 是一个服务器组件
function ServerComponent() {
return <p>这段内容来自服务器组件</p>;
}
// ClientComponent.tsx 是一个客户端组件
("use client");
function ClientComponent({ children }: { children: React.ReactNode }) {
return <div className="client-box">{children}</div>;
}
// page.tsx 里可以这样组合
function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
);
}
注意:只有
children本身可以是 Server Component,Client Component 的内部状态、事件处理仍然需要在客户端运行。
6.3 use Hook 和 children 无关
React 19 新引入了 use Hook,用于读取 Promise、Context 等。它本身不是卡槽机制,但经常和卡槽一起出现在异步数据或 Server Components 的场景中,不要被混淆。
7. 常见大坑:这些地方最容易踩雷
7.1 不要把 children 直接当成数组用
这是新手最常犯的错误:
// 错误示范
function Wrong({ children }: { children: React.ReactNode }) {
// 如果只有一个子节点,children 不是数组,会报错
return <div>{children.length}</div>;
}
正确做法:
function Right({ children }: { children: React.ReactNode }) {
const count = React.Children.count(children);
return <div>子节点数量:{count}</div>;
}
7.2 条件渲染时的 0 和 false
function Demo({ items }: { items: string[] }) {
return (
<div>
{/* 错误:items.length 为 0 时,页面上会显示一个 0 */}
{items.length && <p>有 {items.length} 条数据</p>}
</div>
);
}
正确做法:
function Demo({ items }: { items: string[] }) {
return <div>{items.length > 0 && <p>有 {items.length} 条数据</p>}</div>;
}
原因:React 会渲染
0、NaN、''这些值,但会忽略null、undefined、true、false。
7.3 给列表 children 加 key
当使用 React.Children.map 或直接渲染数组时,必须给每个元素加 key。
function BadList({ items }: { items: string[] }) {
return (
<ul>
{items.map((item) => (
<li>{item}</li> // 警告:缺少 key
))}
</ul>
);
}
function GoodList({ items }: { items: string[] }) {
return (
<ul>
{items.map((item, index) => (
<li key={item}>{item}</li> // 用数据本身作为 key 更好
))}
</ul>
);
}
如果数组中元素没有稳定的唯一标识,再退而求其次用
index,但能用数据本身的唯一值就用唯一值。
7.4 类型写太窄或太宽
// 太宽:any 会丢失类型检查
function TooLoose({ children }: { children: any }) {
return <div>{children}</div>;
}
// 太窄:只允许单个元素,传字符串会报错
function TooNarrow({ children }: { children: React.ReactElement }) {
return <div>{children}</div>;
}
// 刚刚好:任何合法 JSX 内容都能传
function JustRight({ children }: { children: React.ReactNode }) {
return <div>{children}</div>;
}
7.5 cloneElement 修改 children 会破坏预期
React.cloneElement 可以复制一个元素并修改它的 props,包括 children。但如果不注意,会覆盖原有 children 或导致 key 丢失。
function AddBorder({ children }: { children: React.ReactElement }) {
// 这会覆盖子元素原本的 children
return React.cloneElement(children, { className: "border" });
}
如果想同时保留原有 children,需要显式传入:
function AddBorder({ children }: { children: React.ReactElement }) {
return React.cloneElement(children, {
className: "border",
children: children.props.children,
});
}
React 19 官方仍然支持
cloneElement,但建议尽量用 props 传递内容,避免频繁修改已有的 children。
7.6 函数 children 的调用方式
Render Props 中,children 是函数,必须手动调用。
function RenderPropsDemo({
children,
}: {
children: (name: string) => React.ReactNode;
}) {
const name = "React";
// 正确:children 是函数,需要调用
return <div>{children(name)}</div>;
// 错误:直接把函数当 JSX 渲染会报错
// return <div>{children}</div>;
}
7.7 不要在渲染过程中修改 children 数组
children 数组一旦传入,就不要直接 push、pop、splice 它。React 期望 props 是不可变的。
// 错误:children 被修改了
function Bad({ children }: { children: React.ReactNode }) {
const arr = React.Children.toArray(children);
arr.reverse(); // 修改了原数组!
return <div>{arr}</div>;
}
// 正确:复制一份再修改
function Good({ children }: { children: React.ReactNode }) {
const arr = [...React.Children.toArray(children)];
arr.reverse();
return <div>{arr}</div>;
}
8. 小结:什么时候用什么写法?
| 场景 | 推荐写法 |
|---|---|
| 组件只有一个内容区域 | 用 children 默认卡槽 |
| 组件需要多个不同位置的内容 | 用多个命名 props 作为卡槽 |
| 父组件需要把数据或状态交给子组件渲染 | 用函数 children(Render Props) |
| 需要遍历、计数、限制子节点 | 用 React.Children API |
| 卡槽没传内容时给默认提示 | 用 children ?? 默认内容 |
| 不确定传什么类型最安全 | 用 React.ReactNode 类型 |
9. 一句话收尾
React 19 里的“卡槽”不是新 API,而是 children prop 和 JSX 组合这套组合式编程思想的延续。理解了 children 的类型、用法和常见坑,你就掌握了 React 组件设计的核心能力之一。