陷阱

使用 Children 并不常见,并且可能导致代码脆弱。 查看常见的替代方案。

Children 允许你操作和转换作为 children 属性接收到的 JSX。

const mappedChildren = Children.map(children, child =>
<div className="Row">
{child}
</div>
);

参考

Children.count(children)

调用 Children.count(children) 来统计 children 数据结构中的子级数量。

import { Children } from 'react';

function RowList({ children }) {
return (
<>
<h1>Total rows: {Children.count(children)}</h1>
...
</>
);
}

请在下方查看更多示例。

参数

返回值

这些 children 中的节点数量。

注意事项

  • 空节点(nullundefined 和布尔值)、字符串、数字和 React 元素 都算作单个节点。数组不算作单个节点,但它们的子级算作。 遍历不会深入到 React 元素内部: 它们不会被渲染,它们的子级也不会被遍历。 片段 不会被遍历。

Children.forEach(children, fn, thisArg?)

调用 Children.forEach(children, fn, thisArg?)children 数据结构中的每个子节点运行一段代码。

import { Children } from 'react';

function SeparatorList({ children }) {
const result = [];
Children.forEach(children, (child, index) => {
result.push(child);
result.push(<hr key={index} />);
});
// ...

请在下方查看更多示例。

参数

  • children:你的组件接收到的 children 属性的值。
  • fn: 你希望为每个子节点运行的函数,类似于 数组 forEach 方法 的回调函数。它将以子节点作为第一个参数,子节点索引作为第二个参数被调用。索引从 0 开始,并在每次调用时递增。
  • 可选 thisArg: this,用于调用 fn 函数。如果省略,则为 undefined

返回值

Children.forEach 返回 undefined

注意事项

  • 空节点(nullundefined 和布尔值)、字符串、数字和 React 元素 都算作单个节点。数组不算作单个节点,但它们的子级算作。 遍历不会深入到 React 元素内部: 它们不会被渲染,它们的子级也不会被遍历。 片段 不会被遍历。

Children.map(children, fn, thisArg?)

调用 Children.map(children, fn, thisArg?) 来映射或转换 children 数据结构中的每个子节点。

import { Children } from 'react';

function RowList({ children }) {
return (
<div className="RowList">
{Children.map(children, child =>
<div className="Row">
{child}
</div>
)}
</div>
);
}

请在下方查看更多示例。

参数

  • children:你的组件接收到的 children 属性的值。
  • fn: 映射函数,类似于 数组 map 方法 的回调函数。它将以子节点作为第一个参数,子节点索引作为第二个参数被调用。索引从 0 开始,并在每次调用时递增。你需要从此函数返回一个 React 节点。这可以是一个空节点(nullundefined 或布尔值)、字符串、数字、React 元素或其他 React 节点的数组。
  • 可选 thisArg: this,用于调用 fn 函数。如果省略,则为 undefined

返回值

如果 childrennullundefined,则返回相同的值。

否则,返回一个由从 fn 函数返回的节点组成的扁平数组。返回的数组将包含你返回的所有节点,除了 nullundefined

注意事项

  • 空节点(nullundefined 和布尔值)、字符串、数字和 React 元素 都算作单个节点。数组不算作单个节点,但它们的子级算作。 遍历不会深入到 React 元素内部: 它们不会被渲染,它们的子级也不会被遍历。 片段 不会被遍历。

  • 如果你从 fn 中返回一个元素或一个带有键的元素数组,则返回的元素的键将自动与 children 中相应原始项的键组合在一起。 当你从 fn 中返回数组中的多个元素时,它们的键只需要在彼此之间局部唯一。


Children.only(children)

调用 Children.only(children) 来断言 children 表示单个 React 元素。

function Box({ children }) {
const element = Children.only(children);
// ...

参数

返回值

如果 children 是一个有效的元素,则返回该元素。

否则,抛出一个错误。

注意事项

  • 如果将数组(例如 Children.map 的返回值)作为 children 传递,则此方法始终抛出错误。 换句话说,它强制要求 children 是单个 React 元素,而不是包含单个元素的数组。

Children.toArray(children)

调用 Children.toArray(children) 以从 children 数据结构中创建一个数组。

import { Children } from 'react';

export default function ReversedList({ children }) {
const result = Children.toArray(children);
result.reverse();
// ...

参数

返回值

返回 children 中元素的扁平数组。

注意事项

  • 空节点(nullundefined 和布尔值)将在返回的数组中被省略。返回元素的键将根据原始元素的键及其嵌套级别和位置来计算。 这可以确保扁平化数组不会引入行为更改。

用法

转换子元素

要转换组件接收为 children 属性 的子元素 JSX,请调用

import { Children } from 'react';

function RowList({ children }) {
return (
<div className="RowList">
{Children.map(children, child =>
<div className="Row">
{child}
</div>
)}
</div>
);
}

在上面的示例中,RowList 将其接收到的每个子元素都包装到 <div className="Row"> 容器中。例如,假设父组件传递了三个 <p> 标签作为 children prop 传递给 RowList

<RowList>
<p>This is the first item.</p>
<p>This is the second item.</p>
<p>This is the third item.</p>
</RowList>

然后,使用上面的 RowList 实现,最终渲染结果将如下所示

<div className="RowList">
<div className="Row">
<p>This is the first item.</p>
</div>
<div className="Row">
<p>This is the second item.</p>
</div>
<div className="Row">
<p>This is the third item.</p>
</div>
</div>

Children.map 类似于 使用 map() 转换数组。 区别在于 children 数据结构被认为是*不透明的。* 这意味着即使它有时是数组,您也不应该假设它是数组或任何其他特定数据类型。这就是为什么如果需要转换它,则应使用 Children.map 的原因。

import { Children } from 'react';

export default function RowList({ children }) {
  return (
    <div className="RowList">
      {Children.map(children, child =>
        <div className="Row">
          {child}
        </div>
      )}
    </div>
  );
}

深入探讨

为什么 children prop 不一定是数组?

在 React 中,children prop 被认为是一个*不透明的*数据结构。这意味着您不应该依赖于它的结构方式。要转换、过滤或计数子元素,应使用 Children 方法。

在实践中,children 数据结构通常在内部表示为数组。但是,如果只有一个子元素,则 React 不会创建额外的数组,因为这会导致不必要的内存开销。只要您使用 Children 方法而不是直接检查 children prop,即使 React 更改了数据结构的实际实现方式,您的代码也不会中断。

即使 children 是数组,Children.map 也有一些有用的特殊行为。例如,Children.map 将返回元素上的 与您传递给它的 children 上的键组合在一起。这确保了即使原始 JSX 子元素像上面示例中那样被包装,它们也不会“丢失”键。

陷阱

children 数据结构不包含您作为 JSX 传递的组件的**渲染输出**。在下面的示例中,RowList 接收到的 children 仅包含两个项目,而不是三个

  1. <p>这是第一个项目。</p>
  2. <MoreRows />

这就是为什么在这个例子中只生成了两个行包装器的原因

import RowList from './RowList.js';

export default function App() {
  return (
    <RowList>
      <p>This is the first item.</p>
      <MoreRows />
    </RowList>
  );
}

function MoreRows() {
  return (
    <>
      <p>This is the second item.</p>
      <p>This is the third item.</p>
    </>
  );
}

在操作 children **时,无法获取内部组件(如** <MoreRows />**)的渲染输出。**这就是为什么 通常最好使用替代解决方案之一的原因。


为每个子元素运行一些代码

调用 Children.forEach 迭代 children 数据结构中的每个子元素。它不返回任何值,类似于 数组 forEach 方法。 您可以使用它来运行自定义逻辑,例如构建自己的数组。

import { Children } from 'react';

export default function SeparatorList({ children }) {
  const result = [];
  Children.forEach(children, (child, index) => {
    result.push(child);
    result.push(<hr key={index} />);
  });
  result.pop(); // Remove the last separator
  return result;
}

陷阱

如前所述,在操作 children 时,无法获取内部组件的渲染输出。这就是为什么 通常最好使用替代解决方案之一的原因。


计数子元素

调用 Children.count(children) 计算子元素的数量。

import { Children } from 'react';

export default function RowList({ children }) {
  return (
    <div className="RowList">
      <h1 className="RowListHeader">
        Total rows: {Children.count(children)}
      </h1>
      {Children.map(children, child =>
        <div className="Row">
          {child}
        </div>
      )}
    </div>
  );
}

陷阱

如前所述,在操作 children 时,无法获取内部组件的渲染输出。这就是为什么 通常最好使用替代解决方案之一的原因。


将子元素转换为数组

调用 Children.toArray(children)children 数据结构转换为常规 JavaScript 数组。这允许您使用内置数组方法(如 filtersortreverse)操作数组。

import { Children } from 'react';

export default function ReversedList({ children }) {
  const result = Children.toArray(children);
  result.reverse();
  return result;
}

陷阱

如前所述,在操作 children 时,无法获取内部组件的渲染输出。这就是为什么 通常最好使用替代解决方案之一的原因。


替代方案

注意

本节介绍 Children API(首字母 C 大写)的替代方案,该 API 的导入方式如下:

import { Children } from 'react';

不要将其与 使用 children 属性(首字母 c 小写)混淆,后者是良好且鼓励的做法。

公开多个组件

使用 Children 方法操作子元素通常会导致代码脆弱。当您在 JSX 中将子元素传递给组件时,您通常不希望组件操作或转换单个子元素。

如果可以,请尽量避免使用 Children 方法。例如,如果您希望将 RowList 的每个子元素都包装在 <div className="Row"> 中,请导出一个 Row 组件,并像这样手动将每行包装到其中:

import { RowList, Row } from './RowList.js';

export default function App() {
  return (
    <RowList>
      <Row>
        <p>This is the first item.</p>
      </Row>
      <Row>
        <p>This is the second item.</p>
      </Row>
      <Row>
        <p>This is the third item.</p>
      </Row>
    </RowList>
  );
}

与使用 Children.map 不同,此方法不会自动包装每个子元素。但是,与 前面使用 Children.map 的示例 相比,此方法有一个显著的优势,因为它即使您继续提取更多组件也能正常工作。例如,即使您提取了自己的 MoreRows 组件,它仍然可以工作:

import { RowList, Row } from './RowList.js';

export default function App() {
  return (
    <RowList>
      <Row>
        <p>This is the first item.</p>
      </Row>
      <MoreRows />
    </RowList>
  );
}

function MoreRows() {
  return (
    <>
      <Row>
        <p>This is the second item.</p>
      </Row>
      <Row>
        <p>This is the third item.</p>
      </Row>
    </>
  );
}

这对于 Children.map 来说是行不通的,因为它会将 <MoreRows /> 视为单个子元素(和单个行)。


接受对象数组作为属性

您也可以显式传递一个数组作为属性。例如,此 RowList 接受一个 rows 数组作为属性:

import { RowList, Row } from './RowList.js';

export default function App() {
  return (
    <RowList rows={[
      { id: 'first', content: <p>This is the first item.</p> },
      { id: 'second', content: <p>This is the second item.</p> },
      { id: 'third', content: <p>This is the third item.</p> }
    ]} />
  );
}

由于 rows 是一个常规 JavaScript 数组,因此 RowList 组件可以使用内置数组方法(如 map)对其进行操作。

当您希望能够将更多信息作为结构化数据与子元素一起传递时,此模式特别有用。在下面的示例中,TabSwitcher 组件接收一个对象数组作为 tabs 属性:

import TabSwitcher from './TabSwitcher.js';

export default function App() {
  return (
    <TabSwitcher tabs={[
      {
        id: 'first',
        header: 'First',
        content: <p>This is the first item.</p>
      },
      {
        id: 'second',
        header: 'Second',
        content: <p>This is the second item.</p>
      },
      {
        id: 'third',
        header: 'Third',
        content: <p>This is the third item.</p>
      }
    ]} />
  );
}

与将子元素作为 JSX 传递不同,此方法允许您将一些额外的数据(如 header)与每个项目相关联。因为您直接使用 tabs,并且它是一个数组,所以您不需要 Children 方法。


调用渲染属性以自定义渲染

除了为每个项目生成 JSX 之外,您还可以传递一个返回 JSX 的函数,并在必要时调用该函数。在此示例中,App 组件将 renderContent 函数传递给 TabSwitcher 组件。TabSwitcher 组件仅对选定的选项卡调用 renderContent

import TabSwitcher from './TabSwitcher.js';

export default function App() {
  return (
    <TabSwitcher
      tabIds={['first', 'second', 'third']}
      getHeader={tabId => {
        return tabId[0].toUpperCase() + tabId.slice(1);
      }}
      renderContent={tabId => {
        return <p>This is the {tabId} item.</p>;
      }}
    />
  );
}

renderContent 这样的属性被称为“渲染属性”,因为它是一个指定如何渲染用户界面一部分的属性。但是,它并没有什么特别之处:它只是一个恰好是函数的常规属性。

渲染属性是函数,因此您可以向它们传递信息。例如,这个 RowList 组件将每行的 idindex 传递给 renderRow 渲染属性,该属性使用 index 来突出显示偶数行

import { RowList, Row } from './RowList.js';

export default function App() {
  return (
    <RowList
      rowIds={['first', 'second', 'third']}
      renderRow={(id, index) => {
        return (
          <Row isHighlighted={index % 2 === 0}>
            <p>This is the {id} item.</p>
          </Row> 
        );
      }}
    />
  );
}

这是父子组件如何在不操作子组件的情况下进行协作的另一个示例。


故障排除

我传递了一个自定义组件,但 Children 方法没有显示其渲染结果

假设您将两个子级传递给 RowList,如下所示

<RowList>
<p>First item</p>
<MoreRows />
</RowList>

如果您在 RowList 内部执行 Children.count(children),您将得到 2。即使 MoreRows 渲染了 10 个不同的项目,或者它返回 nullChildren.count(children) 仍然是 2。从 RowList 的角度来看,它只能“看到”它接收到的 JSX。它“看不到” MoreRows 组件的内部。

这种限制使得提取组件变得困难。这就是为什么 替代方案 比使用 Children 更受欢迎的原因。