• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

JSX 的标签体部分会通过 children 的 props 传给组件:

在组件里取出 props.children 渲染:

但有的时候,我们要对 children 做一些修改。

比如 Space 组件,传入的是 3 个 .box 的 div:

但渲染出来的 .box 外面包了一层:

这种就需要用 React.Children 的 api 实现。

有这些 api:

  • Children.count(children)
  • Children.forEach(children, fn, thisArg?)
  • Children.map(children, fn, thisArg?)
  • Children.only(children)
  • Children.toArray(children)

我们来试一下。

用 cra 创建个 react 项目:

npx create-react-app --template=typescript children-test

进入项目,改下 index.tsx

import ReactDOM from "react-dom/client";
import App from "./App";

const root = ReactDOM.createRoot(
    document.getElementById("root") as HTMLElement
);
root.render(<App />);

然后在 App.tsx 里测试下 Children 的 api:

import React, { FC } from "react";

interface AaaProps {
    children: React.ReactNode;
}

const Aaa: FC<AaaProps> = (props) => {
    const { children } = props;

    return (
        <div className="container">
            {React.Children.map(children, (item) => {
                return <div className="item">{item}</div>;
            })}
        </div>
    );
};

function App() {
    return (
        <Aaa>
            <a href="#">111</a>
            <a href="#">222</a>
            <a href="#">333</a>
        </Aaa>
    );
}

export default App;

在传入的 children 外包了一层 .item 的 div。

跑一下:

npm run start

可以看到,渲染出的 children 是修改后的:

有的同学说,直接用数组的 api 可以么?

我们试试:

interface AaaProps {
    children: React.ReactNode[];
}

const Aaa: FC<AaaProps> = (props) => {
    const { children } = props;

    return (
        <div className="container">
            {
                // React.Children.map(children, (item) => {
                children.map((item) => {
                    return <div className="item">{item}</div>;
                })
            }
        </div>
    );
};

要用数组的 api 需要把 children 类型声明为 ReactNode[],然后再用数组的 map 方法:

看起来结果貌似一样?

其实并不是。

首先,因为要用数组方法,所以声明了 children 为 ReactNode[],这就导致了如果 children 只有一个元素会报错:

更重要的是当 children 传数组的时候:

function App() {
    return (
        <Aaa>
            {[
                <span>111</span>,
                <span>333</span>,
                [<span>444</span>, <span>222</span>],
            ]}
        </Aaa>
    );
}

数组的 map 处理后是这样的:

换成 React.Children.map 是这样的:

React.Children.map 会把 children 拍平,而数组的方法不会。

还有一点,有时候直接调用数组的 sort 方法会报错:

import React, { FC } from "react";

interface AaaProps {
    children: React.ReactNode[];
}

const Aaa: FC<AaaProps> = (props) => {
    const { children } = props;

    console.log(children.sort());

    return <div className="container"></div>;
};

function App() {
    return (
        <Aaa>
            {33}
            <span>hello world</span>
            {22}
            {11}
        </Aaa>
    );
}

export default App;

因为 props.children 的元素是只读的,不能重新赋值,所以也就不能排序。

这时候只要用 React.Children.toArray 转成数组就好了:

(这里不用 children 数组方法了,就直接声明为 ReactNode 类型了)

interface AaaProps {
    children: React.ReactNode;
}

const Aaa: FC<AaaProps> = (props) => {
    const { children } = props;

    const arr = React.Children.toArray(children);

    console.log(arr.sort());

    return <div className="container"></div>;
};

综上,直接用数组方法操作 children 有 3 个问题:

  • 用数组的方法需要声明 children 为 ReactNode[] 类型,这样就必须传入多个元素才行,而 React.Children 不用
  • 用数组的方法不会对 children 做拍平,而 React.Children 会
  • 用数组的方法不能做排序,因为 children 的元素是只读的,而用 React.Children.toArray 转成数组就可以了

当然,不用记这些区别,只要操作 children,就用 React.Children 的 api 就行了。

然后再试下其它 React.Children 的 api:

import React, { FC, useEffect } from "react";

interface AaaProps {
    children: React.ReactNode;
}

const Aaa: FC<AaaProps> = (props) => {
    const { children } = props;

    useEffect(() => {
        const count = React.Children.count(children);

        console.log("count", count);

        React.Children.forEach(children, (item, index) => {
            console.log("item" + index, item);
        });

        const first = React.Children.only(children);
        console.log("first", first);
    }, []);

    return <div className="container"></div>;
};

function App() {
    return (
        <Aaa>
            {33}
            <span>hello world</span>
            {22}
            {11}
        </Aaa>
    );
}

export default App;

React.Children.count 是计数,forEach 是遍历、only 是如果 children 不是一个元素就报错。

这些 api 都挺简单的。

有的同学可能会注意到,Children 的 api 也被放到了 Legacy 目录下,并提示用 Children 的 api 会导致代码脆弱,建议用别的方式替代:

我们先看看这些替代方式:

首先,我们用 React.Children 来实现这样的功能:

import React, { FC } from "react";

interface RowListProps {
    children?: React.ReactNode;
}

const RowList: FC<RowListProps> = (props) => {
    const { children } = props;

    return (
        <div className="row-list">
            {React.Children.map(children, (item) => {
                return <div className="row">{item}</div>;
            })}
        </div>
    );
};

function App() {
    return (
        <RowList>
            <div>111</div>
            <div>222</div>
            <div>333</div>
        </RowList>
    );
}

export default App;

对传入的 children 做了一些修改之后渲染。

结果如下:

第一种替代方案是这样的:

import React, { FC } from "react";

interface RowProps {
    children?: React.ReactNode;
}

const Row: FC<RowProps> = (props) => {
    const { children } = props;
    return <div className="row">{children}</div>;
};

interface RowListProps {
    children?: React.ReactNode;
}

const RowList: FC<RowListProps> = (props) => {
    const { children } = props;

    return <div className="row-list">{children}</div>;
};

function App() {
    return (
        <RowList>
            <Row>
                <div>111</div>
            </Row>
            <Row>
                <div>222</div>
            </Row>
            <Row>
                <div>333</div>
            </Row>
        </RowList>
    );
}

export default App;

就是把对 children 包装的那一层封装个组件,然后外面自己来包装。

跑一下:

这样是可以的。

当然,这里的 RowListProps 和 RowProps 都是只有 children,我们直接用内置类型 PropsWithChildren 来简化下:

import React, { FC, PropsWithChildren } from "react";

const Row: FC<PropsWithChildren> = (props) => {
    const { children } = props;
    return <div className="row">{children}</div>;
};

const RowList: FC<PropsWithChildren> = (props) => {
    const { children } = props;

    return <div className="row-list">{children}</div>;
};

function App() {
    return (
        <RowList>
            <Row>
                <div>111</div>
            </Row>
            <Row>
                <div>222</div>
            </Row>
            <Row>
                <div>333</div>
            </Row>
        </RowList>
    );
}

export default App;

第二种方案不使用 chilren 传入具体内容,而是自己定义一个 prop:

import { FC, PropsWithChildren, ReactNode } from "react";

interface RowListProps extends PropsWithChildren {
    items: Array<{
        id: number;
        content: ReactNode;
    }>;
}

const RowList: FC<RowListProps> = (props) => {
    const { items } = props;

    return (
        <div className="row-list">
            {items.map((item) => {
                return (
                    <div className="row" key={item.id}>
                        {item.content}
                    </div>
                );
            })}
        </div>
    );
};

function App() {
    return (
        <RowList
            items={[
                {
                    id: 1,
                    content: <div>111</div>,
                },
                {
                    id: 2,
                    content: <div>222</div>,
                },
                {
                    id: 3,
                    content: <div>333</div>,
                },
            ]}></RowList>
    );
}

export default App;

我们声明了 items 的 props,通过其中的 content 来传入内容。

效果是一样的:

而且还可以通过 render props 来定制渲染逻辑:

import { FC, PropsWithChildren, ReactNode } from "react";

interface Item {
    id: number;
    content: ReactNode;
}

interface RowListProps extends PropsWithChildren {
    items: Array<Item>;
    renderItem: (item: Item) => ReactNode;
}

const RowList: FC<RowListProps> = (props) => {
    const { items, renderItem } = props;

    return (
        <div className="row-list">
            {items.map((item) => {
                return renderItem(item);
            })}
        </div>
    );
};

function App() {
    return (
        <RowList
            items={[
                {
                    id: 1,
                    content: <div>111</div>,
                },
                {
                    id: 2,
                    content: <div>222</div>,
                },
                {
                    id: 3,
                    content: <div>333</div>,
                },
            ]}
            renderItem={(item) => {
                return (
                    <div className="row" key={item.id}>
                        <div className="box">{item.content}</div>
                    </div>
                );
            }}></RowList>
    );
}

export default App;

综上,替代 props.children 有两种方案:

  • 把对 children 的修改封装成一个组件,使用者用它来手动包装
  • 声明一个 props 来接受数据,内部基于它来渲染,而且还可以传入 render props 让使用者定制渲染逻辑

但是这些替代方案使用起来和 React.Children 还是不同的。

React.Children 使用起来是无感的:

而这两种替代方案使用起来是这样的:

虽然能达到同样的效果,但是还是用 React.Children 内部修改 children 的方式更易用一些。

而且现在各大组件库依然都在大量用 React.Children

比如 semi design 的代码:

arco design 的代码:

ant design 的代码:

比如我们上节写过的 Space 组件:

所以 React.Children 还是可以继续用的,因为这些替代方案和 React.Children 还是有差距。

案例代码上传了小册仓库。

总结

我们学了用 React.Children 来修改 children,它有 map、forEach、toArray、only、count 等方法。

不建议直接用数组方法来操作,而是用 React.Children 的 api。

原因有三个:

当然,Children 的 api 被放到了 legacy 目录,可以用这两种方案来替代:

不过,这两种替代方案易用性都不如 React.Children,各大组件库也依然大量使用 React.Children 的 api。

所以,遇到需要修改渲染的 children 的情况,用 React.Children 的 api,或是两种替代方案(抽离渲染逻辑为单独组件、传入数据 + render props)都可以。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
14.组件实战:Space间距组件
Next
16.三个简单组件的封装