Menu

React Fragment 教程:<> 与 <Fragment> 用法

React Fragment 能把多个元素合成一个返回值,又不会往页面里多加节点。学习 <>...</> 简写语法、什么时候需要 <Fragment key>,以及 Fragment 在表格和列表中为什么重要。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

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 会编译成单个表达式,所以要用 <>...</> 把这些相邻元素包起来。

Coddy 编程语言插图

和 Coddy 一起学编程

开始