目 录CONTENT

文章目录

React(六)卡槽

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,会显示默认提示

这里用 ??(空值合并运算符)而不是 ||,因为 || 会把 0false 这些合法值也误判为空。


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 作为默认卡槽。
  • titlefooter 作为命名卡槽。
  • 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 条件渲染时的 0false

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 会渲染 0NaN'' 这些值,但会忽略 nullundefinedtruefalse

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 数组一旦传入,就不要直接 pushpopsplice 它。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 组件设计的核心能力之一。

0
博主关闭了当前页面的评论