Children

操作和检查 children 的工具函数 - 处理不透明的 props.children

核心概述

React.Children 提供了一组工具函数,用于操作 props.children, 这是一个不透明的数据结构。虽然 children 通常表现为数组, 但它实际上可能是单个元素、数组、片段或其他形式,直接使用数组方法会出错。

React.Children 的方法确保你可以安全地遍历、计数、映射和转换 children, 而不需要关心它们的具体数据结构。

⚠️ 注意:在大多数情况下,你应该优先使用 React 18+ 的新特性, 如特殊 props (render, as) 来代替 Children API。 Children API 主要用于向后兼容和特殊场景。

💡 何时使用 Children API

  • • 需要统计或操作 children 的数量
  • • 需要对每个 child 进行转换或克隆
  • • 需要验证 children 的类型或内容
  • • 创建共享布局的组件 (如 Grid, Stack)

API 方法

Children.map

对 children 中的每个直接子节点执行函数,类似于数组的 map 方法。

React.Children.map(children, fn, context?)

参数:

  • children: 要遍历的 children (不透明数据结构)
  • fn: 对每个 child 执行的函数,接收 child 和索引作为参数
  • context: 可选的 this 上下文

返回值:返回新的 children 数组

function RowList({ children }) {
  return (
    <div className="space-y-2">
      {React.Children.map(children, (child, index) => (
        <div key={index} className="row">
          {child}
        </div>
      ))}
    </div>
  );
}

// 使用
<RowList>
  <div>Row 1</div>
  <div>Row 2</div>
  <div>Row 3</div>
</RowList>

Children.forEach

类似于 Children.map,但不返回新数组,用于遍历 side effect。

React.Children.forEach(children, fn, context?)
function FormGroup({ children }) {
  let hasError = false;

  // 检查是否有任何子组件有错误
  React.Children.forEach(children, (child) => {
    if (React.isValidElement(child) && child.props.error) {
      hasError = true;
    }
  });

  return (
    <div className={hasError ? 'has-error' : ''}>
      {children}
    </div>
  );
}

Children.count

返回 children 中的直接子节点数量,忽略嵌套结构。

React.Children.count(children): number
function Breadcrumb({ children }) {
  const count = React.Children.count(children);

  return (
    <nav className="breadcrumb">
      {React.Children.map(children, (child, index) => (
        <React.Fragment key={index}>
          {child}
          {index < count - 1 && <span className="separator">/</span>}
        </React.Fragment>
      ))}
    </nav>
  );
}

// 使用
<Breadcrumb>
  <a href="/">Home</a>
  <a href="/products">Products</a>
  <span>Product Detail</span>
</Breadcrumb>

Children.only

验证 children 只有一个子节点,并返回它。否则抛出错误。

React.Children.only(children): ReactElement

常见用途:确保组件只接收一个子元素。

function Modal({ children }) {
  const child = React.Children.only(children);

  return (
    <div className="modal">
      <div className="modal-content">
        {child}
      </div>
    </div>
  );
}

// ✅ 正确:只有一个子元素
<Modal>
  <div>Content</div>
</Modal>

// ❌ 错误:抛出异常
<Modal>
  <div>Content 1</div>
  <div>Content 2</div>
</Modal>

Children.toArray

将不透明的 children 数据结构转换为扁平的数组,可以安全地使用数组方法。

React.Children.toArray(children): Array

重要:返回的数组保证 key 的稳定性,可以用于排序、过滤等操作。

function SortableList({ children, reverse = false }) {
  // 将 children 转换为数组
  const childArray = React.Children.toArray(children);

  // 排序或反转
  const sortedChildren = reverse ? childArray.reverse() : childArray;

  return (
    <ul>
      {sortedChildren.map((child) => (
        <li key={child.key}>{child}</li>
      ))}
    </ul>
  );
}

// 使用
<SortableList reverse>
  <li key="1">Item 1</li>
  <li key="2">Item 2</li>
  <li key="3">Item 3</li>
</SortableList>

实战案例

1. 创建共享布局组件

使用 Children.map 为每个 child 添加共享布局:

function Grid({ children, cols = 3 }) {
  return (
    <div
      style={{
        display: 'grid',
        gridTemplateColumns: `repeat(${cols}, 1fr)`,
        gap: '1rem',
      }}
    >
      {React.Children.map(children, (child) => (
        <div className="grid-item">{child}</div>
      ))}
    </div>
  );
}

// 使用
<Grid cols={4}>
  <Card>Item 1</Card>
  <Card>Item 2</Card>
  <Card>Item 3</Card>
  <Card>Item 4</Card>
</Grid>

2. 增强 Child Props

使用 Children.map 和 cloneElement 为每个 child 添加额外的 props:

function RadioGroup({ children, name, onChange }) {
  return (
    <div>
      {React.Children.map(children, (child, index) => {
        // 确保 child 是有效的 React 元素
        if (React.isValidElement(child)) {
          // 克隆并添加额外的 props
          return React.cloneElement(child, {
            name,
            onChange,
            // 如果没有 key,使用索引作为 key
            key: child.key || index,
          });
        }
        return child;
      })}
    </div>
  );
}

// 使用
<RadioGroup name="color" onChange={(e) => console.log(e.target.value)}>
  <Radio value="red">Red</Radio>
  <Radio value="green">Green</Radio>
  <Radio value="blue">Blue</Radio>
</RadioGroup>

3. 条件渲染 Children

根据条件过滤或修改 children:

function ConditionalWrapper({ children, condition, wrapper }) {
  // 将 children 转换为数组
  const childArray = React.Children.toArray(children);

  // 根据 condition 包装每个 child
  return (
    <Fragment>
      {childArray.map((child) => {
        if (condition(child)) {
          return wrapper(child);
        }
        return child;
      })}
    </Fragment>
  );
}

// 使用:只包装特定类型的组件
<ConditionalWrapper
  condition={(child) => child.type === 'button'}
  wrapper={(child) => <Tooltip>{child}</Tooltip>}
>
  <button>Hover me</button>
  <span>Not wrapped</span>
  <button>Me too</button>
</ConditionalWrapper>

4. 分隔 Children

在 children 之间添加分隔符:

function Breadcrumbs({ children, separator = '/' }) {
  const childArray = React.Children.toArray(children);
  const count = childArray.length;

  return (
    <nav className="breadcrumbs">
      {childArray.map((child, index) => (
        <React.Fragment key={index}>
          {child}
          {index < count - 1 && (
            <span className="separator">{separator}</span>
          )}
        </React.Fragment>
      ))}
    </nav>
  );
}

// 使用
<Breadcrumbs separator=">">
  <a href="/">Home</a>
  <a href="/products">Products</a>
  <span>Electronics</span>
</Breadcrumbs>

避坑指南

陷阱 1: 直接对 children 使用数组方法

问题:props.children 不是真正的数组,直接使用数组方法会出错。

// ❌ 错误:children 可能不是数组
function RowList({ children }) {
  return (
    <div>
      {children.map((child) => ( // 可能抛出 "children.map is not a function"
        <div key={child.key}>{child}</div>
      ))}
    </div>
  );
}

// ✅ 正确:使用 React.Children.map
function RowList({ children }) {
  return (
    <div>
      {React.Children.map(children, (child) => (
        <div key={child.key}>{child}</div>
      ))}
    </div>
  );
}

陷阱 2: 遍历嵌套的 Fragment

问题:Children.map 只遍历直接子节点,不会展开 Fragment。

function Parent() {
  const children = (
    <Fragment>
      <div>Child 1</div>
      <div>Child 2</div>
    </Fragment>
  );

  // Children.count 会返回 1 (Fragment 本身)
  // 而不是 2 (Fragment 内的元素)
  console.log(React.Children.count(children)); // 1

  // 需要使用 Children.toArray 展开 Fragment
  const array = React.Children.toArray(children);
  console.log(array.length); // 2
}

陷阱 3: 在 map 中修改 child 的 key

问题:在 Children.map 中返回的元素必须有稳定的 key。

// ❌ 错误:没有保留 key
{React.Children.map(children, (child, index) => (
  <div>{child}</div> // 丢失了原始 key
))}

// ✅ 正确:保留原始 key 或生成新的稳定 key
{React.Children.map(children, (child) => (
  <div key={child.key}>{child}</div>
))}

// 或者使用 cloneElement
{React.Children.map(children, (child) =>
  React.cloneElement(child, {
    ...child.props,
    className: 'enhanced',
  })
)}

最佳实践

✅ 推荐模式

  • 使用 Children API 处理不透明 children 数据结构
  • 优先使用特殊 props (render, as) 代替 Children.map
  • 在 cloneElement 时保留原有的 props
  • 使用 Children.toArray 进行排序、过滤等操作

❌ 避免模式

  • 不要直接对 children 使用数组方法
  • 不要过度使用 Children.map (考虑 render prop)
  • 不要在 map 中修改 child 的 key (除非你知道后果)
  • 不要依赖 children 的顺序 (除非你使用 toArray)

替代方案

在很多情况下,有更好的替代方案来使用 Children API:

使用 render prop

// ❌ 使用 Children.map
function Grid({ children }) {
  return (
    <div className="grid">
      {React.Children.map(children, (child) => (
        <GridItem>{child}</GridItem>
      ))}
    </div>
  );
}

// ✅ 使用 render prop
function Grid({ items, renderItem }) {
  return (
    <div className="grid">
      {items.map((item, index) => (
        <GridItem key={index}>{renderItem(item, index)}</GridItem>
      ))}
    </div>
  );
}

// 使用
<Grid
  items={[1, 2, 3]}
  renderItem={(item, index) => <div key={index}>{item}</div>}
/>

使用 compound components

// ✅ 使用 compound components
function Tabs({ children }) {
  const [activeTab, setActiveTab] = useState(0);

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      {children}
    </TabsContext.Provider>
  );
}

Tabs.List = function TabList({ children }) {
  return <div className="tab-list">{children}</div>;
};

Tabs.Tab = function Tab({ index, children }) {
  const { activeTab, setActiveTab } = useContext(TabsContext);

  return (
    <button
      className={activeTab === index ? 'active' : ''}
      onClick={() => setActiveTab(index)}
    >
      {children}
    </button>
  );
};

// 使用
<Tabs>
  <Tabs.List>
    <Tabs.Tab index={0}>Tab 1</Tabs.Tab>
    <Tabs.Tab index={1}>Tab 2</Tabs.Tab>
  </Tabs.List>
</Tabs>

延伸阅读

这篇文章有帮助吗?