React Fragment 让组件可以返回多个元素,而不必再额外套一层 <div>。在第一个元素前写 <>,在最后一个元素后写 </>,React 就会把这些元素直接放进父元素,页面里不会出现 Fragment 自己的节点。
每个 Greeting 都并排返回一个标题和一个段落。删掉 <> 和 </> 这两行,预览就会显示构建错误,因为这个函数现在想返回两个值。
组件为什么需要一个根元素
JSX 是函数调用的一种更好看的写法。<h2>Hi</h2> 大致会编译成 jsx('h2', { children: 'Hi' }),也就是一个表达式。return 语句只能返回一个值,所以两个外面没有任何包裹的相邻标签,就相当于写了 return a b:编译器会报错“Adjacent JSX elements must be wrapped in an enclosing tag”。
在有 Fragment 之前,解决办法是套一个 <div>。这样能用,但 div 是真实的元素。它会出现在 DOM 中,会参与 CSS 布局,有些父元素根本不接受它作为子元素。Fragment 满足了“只能有一个根”的规则,然后就消失了。
你可以亲眼看到它什么都没添加。下面的例子分别用 Fragment 和 div 渲染同样的两个元素,然后统计每个容器的直接子元素数量。
控制台对 Fragment 打印 2,也就是那两个 span;对 div 打印 1,也就是包住它们的 div。在 flex 或 grid 容器中,这个差别决定了两个 span 是各自作为布局项排列,还是被打包成一项。
简写语法与 Fragment 组件
Fragment 有两种写法,编译结果相同:
import { Fragment } from 'react';
// Short syntax, no import needed
<>
<dt>React</dt>
<dd>A library for building user interfaces</dd>
</>
// Long form
<Fragment>
<dt>React</dt>
<dd>A library for building user interfaces</dd>
</Fragment>
几乎所有地方都用 <>...</>。只有一种情况要用 <Fragment>:Fragment 需要 key 的时候。简写语法不能带属性,所以 <key={id}> 不是合法的 JSX。在旧代码里你还可能看到 <React.Fragment>,它是同一个组件,只是通过默认导入的 React 来访问。
列表中带 key 的 Fragment
用 map 渲染列表时,每一项都需要一个 key,这样 React 才能在多次渲染之间追踪它(原因见列表与 key 一页)。如果每一项是一组元素而不是一个元素,key 就要放在 Fragment 上,所以需要用完整写法。
描述列表是最典型的例子:每个条目是一个 <dt> 加一个 <dd>,而 <dl> 应该直接包含它们。
点击 Reverse,每一对会作为整体交换位置,因为每一对都有自己的 key。给每一对套一个 <div> 也能带 key,而且在 dl 中是合法的 HTML,但这会给标记多加一层,而 Fragment 不会。
表格中的 Fragment
多余的包裹元素在表格里危害最大。<tr> 要求它的直接子元素是 <td> 和 <th> 单元格。如果一个返回多个单元格的组件用 <div> 把它们包起来,浏览器会把这个 div 当成一个奇怪的单元格,里面的单元格就无法再和表头对齐。
在 Fragment 那一行,价格在 Price 列下,库存在 Stock 列下。而在 div 那一行,两个单元格都挤进了 Price 列,Stock 列是空的。在开发环境中,React 还会打印一条警告,提示 <div> 不能作为 <tr> 的子元素;这里的预览以生产构建的方式运行,会跳过这项检查,所以你看到的就是错乱的布局。如果使用服务端渲染,同样的错误还可能导致 hydration 错误,因为浏览器的 HTML 解析器会把放错位置的 div 移走。
同样的规则也适用于 <ul> 和 <ol>(应该包含 <li> 元素)以及 <select>(应该包含 <option> 元素)。只要组件返回的内容应该直接放在这类父元素里,就返回一个 Fragment。
Fragment 还是数组
组件也可以返回一个元素数组,React 会像处理 Fragment 一样按顺序渲染它们。返回的数组中每个元素都需要自己的 key,和 map 的结果一样,因为 React 会把数组当作列表:
function Cells() {
return [<td key="price">$4</td>, <td key="stock">In stock</td>];
}
这样能用,但更啰嗦:元素之间要加逗号,每个元素都要带 key,标记一多,方括号也很容易漏看。固定的一组元素用 Fragment,数组就留给它最自然的用途:对数据做 map 的结果。
Fragment 做不到的事
因为 Fragment 不渲染任何元素,所以没有地方放 class、样式、事件处理函数或 ref。<Fragment className="row"> 没有任何实际作用,React 从 Fragment 上读取的唯一 prop 就是 key。如果需要给这一组元素加样式或监听点击,那就确实需要一个真实的元素,这完全没问题:选一个放在那个位置合法的元素,比如用 <tbody> 来组合多行,用 <li> 来放列表项。
Fragment 能用在任何可以放元素的地方,包括条件渲染中:{show && <>...</>} 会一次性显示或隐藏整组元素。
常见问题
React 中的 Fragment 是什么?
Fragment 是一种包裹方式,让组件可以把多个元素作为一个值返回,同时不添加 DOM 节点。<><h1 /><p /></> 会把 h1 和 p 直接放进父元素。
<> 和 <Fragment> 有什么区别?
两者的结果完全一样。<>...</> 是简写语法,<Fragment> 是需要从 react 导入的完整写法,而且只有它能接收 key,在列表中渲染 Fragment 时就需要它。
为什么用 Fragment 而不是 div?
div 是真实的元素。它可能破坏要求直接子元素的布局(flex 和 grid 容器、ul、table、tr),还会让 DOM 变得杂乱。Fragment 什么都不会添加。
Fragment 能加 className 或 onClick 吗?
不能。Fragment 不渲染任何元素,所以没有地方放 class、样式或事件处理函数。它唯一接受的 prop 是 key。如果需要这些,请使用真实的元素。
为什么会出现“Adjacent JSX elements must be wrapped in an enclosing tag”错误?
组件返回了两个相邻的元素,外面没有任何包裹。JSX 会编译成单个表达式,所以要用 <>...</> 把这些相邻元素包起来。