子元素(Children)

陷阱

使用 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 开始,每次调用递增。
  • 可选 thisArgthisfn 函数应该以此值来调用。如果省略,则为 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 节点的数组。
  • 可选 thisArgthisfn 函数应该以此值来调用。如果省略,则为 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,请调用 Children.map

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 属性传递给 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 属性并不总是数组?

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

实际上,children 数据结构通常在内部表示为数组。但是,如果只有一个子元素,那么 React 不会创建额外的数组,因为这会导致不必要的内存开销。只要使用 Children 方法而不是直接检查 children 属性,即使 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>
    </>
  );
}

无法获取内部组件(例如 <MoreRows />)的渲染输出,当操作 children 时。这就是为什么 通常最好使用其中一种替代方案。


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

调用 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)的替代方案,其导入方式如下:

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 方法。


调用 render prop 自定义渲染

无需为每个项目都生成 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 这样的 prop 称为 *render prop*,因为它是一个指定如何渲染用户界面一部分的 prop。但是,它没有什么特别之处:它只是一个碰巧是函数的普通 prop。

Render prop 是函数,因此您可以向其传递信息。例如,此 RowList 组件将每行的 idindex 传递给 renderRow render prop,后者使用 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) 仍然是 2RowList 的角度来看,它只“看到”它收到的 JSX。它没有“看到”MoreRows 组件的内部结构。

此限制使得难以提取组件。这就是为什么 替代方案 比使用 Children 更受青睐。