Fragment

Fragment 让你在不添加额外 DOM 节点的情况下对子元素进行分组。

核心概述

⚠️ 痛点: React 组件必须返回单个元素

  • • JSX 要求组件必须有唯一的根元素
  • • 使用 div 包裹会创建额外的 DOM 节点
  • • 额外的 DOM 节点可能破坏 HTML 结构(如 table, ul)
  • • 影响 CSS Grid 和 Flexbox 布局

✅ 解决方案: Fragment 无包裹分组

  • 不创建额外 DOM: Fragment 不会渲染真实节点
  • 支持简写: <></> 更简洁
  • 保持语义: 不破坏 HTML 结构
  • 支持 key: 列表中可使用带 key 的 Fragment

💡 心智模型: 透明包装纸

将 Fragment 想象成"透明包装纸":

  • 可以包裹多个物品: 允许返回多个子元素
  • 完全透明: 在 DOM 中不可见
  • 不占用空间: 不影响布局
  • 可以标记: 支持 key 属性标识

技术规格

语法

// 完整形式
<React.Fragment>
  {/* 子元素 */}
</React.Fragment>

// 简写形式
<Fragment>
  {/* 子元素 */}
</Fragment>

// 带有 key 的 Fragment
<React.Fragment key={uniqueKey}>
  {/* 子元素 */}
</React.Fragment>

限制

特性支持情况
key 属性✅ 支持(仅完整形式)
其他属性(className, style 等)❌ 不支持
简写形式 <></>⚠️ 不支持任何属性

实战演练

示例 1: 基础用法

// ❌ 错误: 不能返回多个元素
function Wrong() {
  return (
    <h1>标题</h1>
    <p>段落</p>
  );
}

// ⚠️ 可行但创建额外 DOM
function WrappedWithDiv() {
  return (
    <div>
      <h1>标题</h1>
      <p>段落</p>
    </div>
  );
}

// ✅ 推荐: 使用 Fragment 简写
function Correct() {
  return (
    <Fragment>
      <h1>标题</h1>
      <p>段落</p>
    </Fragment>
  );
}

// ✅ 完整形式
function WithKey({ items }: { items: Item[] }) {
  return (
    <React.Fragment key={items[0].id}>
      <dt>{items[0].term}</dt>
      <dd>{items[0].description}</dd>
    </React.Fragment>
  );
}

示例 2: 保持 HTML 语义

// ❌ 错误: div 不能作为 table 的直接子元素
function BrokenTable() {
  return (
    <table>
      <div>
        <tr><td>行 1</td></tr>
        <tr><td>行 2</td></tr>
      </div>
    </table>
  );
}

// ✅ 正确: 使用 Fragment
function CorrectTable() {
  return (
    <table>
      <tbody>
        <Fragment>
          <tr><td>行 1</td></tr>
          <tr><td>行 2</td></tr>
        </Fragment>
      </tbody>
    </table>
  );
}

// ✅ 列表渲染使用带 key 的 Fragment
function Glossary({ items }: { items: { id: string; term: string; description: string }[] }) {
  return (
    <dl>
      {items.map(item => (
        <React.Fragment key={item.id}>
          <dt>{item.term}</dt>
          <dd>{item.description}</dd>
        </React.Fragment>
      ))}
    </dl>
  );
}

示例 3: 条件渲染多个元素

function UserInfo({ user }: { user: User | null }) {
  return (
    <Fragment>
      {user ? (
        <Fragment>
          <Avatar src={user.avatar} />
          <h2>{user.name}</h2>
          <p>{user.email}</p>
          <button onClick={() => editProfile(user)}>编辑</button>
        </Fragment>
      ) : (
        <Fragment>
          <h2>访客</h2>
          <p>请登录以查看更多信息</p>
          <button onClick={login}>登录</button>
          <button onClick={register}>注册</button>
        </Fragment>
      )}
    </Fragment>
  );
}

// 优点:
// 1. 避免创建额外的 DOM 节点
// 2. 保持代码结构清晰
// 3. CSS Grid/Flexbox 布局不受影响

避坑指南

❌ 错误 1: 给简写 Fragment 添加属性

// ❌ 错误: 简写形式不支持属性
function ErrorExample() {
  return (
    <Fragment>
      <div>Item 1</div>
      <div>Item 2</div>
    </Fragment>
  );
}

// 问题:
// 1. TypeScript 会报错
// 2. Babel 会编译失败
// 3. React 无法识别属性

✅ 正确 1: 使用完整形式添加属性

// ✅ 正确: 使用完整形式
function CorrectExample({ items }) {
  return (
    <React.Fragment key={items[0].id}>
      <div>Item 1</div>
      <div>Item 2</div>
    </React.Fragment>
  );
}

// 优点:
// 1. 可以添加 key 属性
// 2. 语义更明确
// 3. TypeScript 不会报错

❌ 错误 2: 过度使用 Fragment

// ❌ 错误: 单个元素不需要 Fragment
function OverusedFragment() {
  return (
    <Fragment>
      <div>单个元素</div>
    </Fragment>
  );
}

// 问题:
// 1. 增加代码复杂度
// 2. 没有任何好处
// 3. 降低可读性

✅ 正确 2: 只在需要时使用 Fragment

// ✅ 正确: 单个元素直接返回
function SingleElement() {
  return <div>单个元素</div>;
}

// ✅ 正确: 多个元素使用 Fragment
function MultipleElements() {
  return (
    <Fragment>
      <div>元素 1</div>
      <div>元素 2</div>
    </Fragment>
  );
}

// 优点:
// 1. 代码简洁
// 2. 语义清晰
// 3. 性能最优

❌ 错误 3: 期望 Fragment 支持样式

// ❌ 错误: Fragment 不支持 className
function ErrorExample() {
  return (
    <React.Fragment className="container">
      <div>Item 1</div>
      <div>Item 2</div>
    </React.Fragment>
  );
}

// 问题:
// 1. className 会被忽略
// 2. 样式不会生效
// 3. 容易产生 bug

✅ 正确 3: 使用 div 添加样式

// ✅ 正确: 需要样式时使用 div
function StyledWrapper() {
  return (
    <div className="container flex gap-4">
      <div>Item 1</div>
      <div>Item 2</div>
    </div>
  );
}

// ✅ 正确: Fragment 用于无样式包裹
function UnstyledGroup() {
  return (
    <Fragment>
      <div>Item 1</div>
      <div>Item 2</div>
    </Fragment>
  );
}

// 使用建议:
// - 需要样式/事件监听 → 使用 div
// - 只需分组 → 使用 Fragment

最佳实践

1. Fragment vs 其他方案对比

方案额外 DOM支持属性适用场景
Fragment❌ 无仅 key✅ 大多数场景
<div>✅ 有全部需要样式/事件
数组❌ 无⚠️ 不推荐(需要 key)

2. 常见使用场景

// 场景 1: 表格行列
function TableRow() {
  return (
    <tr>
      <Fragment>
        <td>单元格 1</td>
        <td>单元格 2</td>
        <td>单元格 3</td>
      </Fragment>
    </tr>
  );
}

// 场景 2: 定义列表
function DefinitionList({ items }) {
  return (
    <dl>
      {items.map(item => (
        <Fragment key={item.id}>
          <dt>{item.term}</dt>
          <dd>{item.description}</dd>
        </Fragment>
      ))}
    </dl>
  );
}

// 场景 3: CSS Flexbox 容器
function FlexContainer({ children }) {
  return (
    <div style={{ display: 'flex' }}>
      {/* Fragment 不会影响 Flexbox 布局 */}
      {children}
    </div>
  );
}

// 场景 4: 条件渲染多个元素
function Conditional({ showDetails }) {
  return (
    <Fragment>
      <h1>标题</h1>
      {showDetails && (
        <Fragment>
          <p>详细信息 1</p>
          <p>详细信息 2</p>
        </Fragment>
      )}
    </Fragment>
  );
}

3. 性能考虑

// Fragment vs div 性能对比

// 使用 div - 创建额外 DOM 节点
function WithDiv({ items }) {
  return (
    <div>
      {items.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

// 使用 Fragment - 无额外 DOM
function WithFragment({ items }) {
  return (
    <Fragment>
      {items.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </Fragment>
  );
}

// 性能差异:
// 1. 内存使用: Fragment < div
// 2. 渲染时间: Fragment ≈ div(差异极小)
// 3. DOM 大小: Fragment 更小

// 建议:
// - 大型列表优先使用 Fragment
// - 性能敏感场景使用 Fragment
// - 需要样式时使用 div

相关链接