<Fragment> (<>...</>)

<Fragment>,通常通过 <>...</> 语法使用,可让你将元素分组而无需额外的包装节点。

Canary

Fragment 还可以接受 refs,这使你能够在不添加包装元素的情况下与底层 DOM 节点交互。
<>
<OneChild />
<AnotherChild />
</>

参考

<Fragment>

使用 <Fragment> 将元素包裹起来,以便在需要单个元素的场景中将它们分组。用 Fragment 对元素进行分组不会对最终的 DOM 产生任何影响;它与这些元素未被分组时是一样的。在大多数情况下,空的 JSX 标签 <></><Fragment></Fragment> 的简写。

属性

  • 可选 key: 以显式 <Fragment> 语法声明的 Fragments 可以有 keys.
  • Canary only 可选 ref: 一个 ref 对象(例如来自 useRef)或 回调函数。React 会提供一个 FragmentInstance 作为 ref 值,它实现了用于与被 Fragment 包裹的 DOM 节点交互的方法。

注意事项

  • 如果你想向 Fragment 传递 key,就不能使用 <>...</> 语法。你必须显式地从 'react' 中导入 Fragment,并渲染 <Fragment key={yourKey}>...</Fragment>

  • 当你从渲染 <><Child /></> 切换到 [<Child />] 或切换回来,或者当你从渲染 <><Child /></> 切换到 <Child /> 再切换回来时,React 不会重置状态。这只在单层深度时有效:例如,从 <><><Child /></></> 切换到 <Child /> 会重置状态。请参阅这里的精确语义

  • Canary only 如果你想向 Fragment 传递 ref,就不能使用 <>...</> 语法。你必须显式地从 'react' 中导入 Fragment,并渲染 <Fragment ref={yourRef}>...</Fragment>


Canary only FragmentInstance

当你将 ref 传给一个 Fragment 时,React 会提供一个 FragmentInstance 对象。它实现了一些用于与 Fragment 包裹的一级 DOM 子节点交互的方法。


addEventListener(type, listener, options?)

向 Fragment 的所有一级 DOM 子节点添加事件监听器。

fragmentRef.current.addEventListener('click', handleClick);
参数
  • type:表示要监听的事件类型的字符串(例如 'click''focus')。
  • listener:事件处理函数。
  • 可选 options:用于捕获阶段的选项对象或布尔值,符合 DOM addEventListener API.
返回值

addEventListener 不返回任何内容(undefined)。


removeEventListener(type, listener, options?)

从 Fragment 的所有一级 DOM 子节点中移除事件监听器。

fragmentRef.current.removeEventListener('click', handleClick);
参数
  • type:事件类型字符串。
  • listener:要移除的事件处理函数。
  • 可选 options:选项对象或布尔值,符合 DOM removeEventListener API.
返回值

removeEventListener 不返回任何内容(undefined)。


dispatchEvent(event)

在 Fragment 上派发一个事件。已添加的事件监听器会被调用,并且该事件可以冒泡到 Fragment 的 DOM 父节点。

fragmentRef.current.dispatchEvent(new Event('custom', { bubbles: true }));
参数
  • event:要派发的一个 Event 对象。如果 bubblestrue,事件会冒泡到 Fragment 的父 DOM 节点。
返回值

如果事件未被取消,则返回 true;如果调用了 preventDefault(),则返回 false


focus(options?)

将焦点设置到 Fragment 中第一个可聚焦的 DOM 节点。与对 DOM 元素调用 element.focus() 不同,此方法会按深度优先方式搜索 所有 嵌套子节点,直到找到一个可聚焦元素——不仅仅是该元素本身或它的直接子节点。

fragmentRef.current.focus();
参数
  • 可选 options:一个 FocusOptions 对象(例如 { preventScroll: true })。
返回值

focus 不返回任何内容(undefined)。


focusLast(options?)

将焦点设置到 Fragment 中最后一个可聚焦的 DOM 节点。按深度优先方式搜索嵌套子节点,然后反向遍历。

fragmentRef.current.focusLast();
参数
返回值

focusLast 不返回任何内容(undefined)。


blur()

如果当前活动元素位于 Fragment 内,则移除其焦点。如果 document.activeElement 不在 Fragment 内,blur 不执行任何操作。

fragmentRef.current.blur();
返回值

blur 不返回任何内容(undefined)。


observeUsing(observer)

使用提供的观察器开始观察 Fragment 的所有一级 DOM 子节点。

const observer = new IntersectionObserver(callback, options);
fragmentRef.current.observeUsing(observer);
参数
返回值

observeUsing 不返回任何内容(undefined)。


unobserveUsing(observer)

停止使用指定观察器观察 Fragment 的 DOM 子节点。

fragmentRef.current.unobserveUsing(observer);
参数
  • observer:之前传给 observeUsing 的同一个 IntersectionObserverResizeObserver 实例。
返回值

unobserveUsing 不返回任何内容(undefined)。


getClientRects()

返回一个由 DOMRect 对象组成的扁平数组,表示所有一级 DOM 子节点的边界矩形。

const rects = fragmentRef.current.getClientRects();
返回值

一个包含所有子节点边界矩形的 Array<DOMRect>


getRootNode(options?)

返回包含 Fragment 父 DOM 节点的根节点,行为与 Node.getRootNode() 一致。

const root = fragmentRef.current.getRootNode();
参数
返回值

一个 DocumentShadowRoot,或者在没有父 DOM 节点时返回 FragmentInstance 本身。


compareDocumentPosition(otherNode)

比较 Fragment 与另一个节点在文档中的位置,返回一个与 Node.compareDocumentPosition() 行为一致的位掩码。

const position = fragmentRef.current.compareDocumentPosition(otherElement);
参数
  • otherNode:要进行比较的 DOM 节点。
返回值

一个 位置标志 的位掩码。空 Fragment 和通过 portal 渲染子节点的 Fragment,结果中包含 Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC


scrollIntoView(alignToTop?)

将 Fragment 的子节点滚动到视图中。当 alignToToptrue 或省略时,会滚动以将第一个子节点与可滚动祖先元素的顶部对齐。当 alignToTopfalse 时,会滚动以将最后一个子节点与底部对齐。

fragmentRef.current.scrollIntoView();
参数
  • 可选 alignToTop:一个布尔值。如果为 true(默认值),会将第一个子节点滚动到可滚动区域顶部。如果为 false,会将最后一个子节点滚动到底部。与 Element.scrollIntoView() 不同,此方法不接受 ScrollIntoViewOptions 对象。
返回值

scrollIntoView 不返回任何内容(undefined)。

注意事项
  • scrollIntoView 不接受选项对象。传入该对象会抛出错误。请改用 alignToTop 布尔值。
  • 当 Fragment 没有子节点时,scrollIntoView 会回退为将最近的兄弟节点或父节点滚动到视图中。

FragmentInstance 注意事项

  • 作用于子节点的方法(例如 addEventListenerobserveUsinggetClientRects)只会作用于 Fragment 的一级宿主(DOM)子节点。它们不会直接作用于嵌套在另一个 DOM 元素中的子节点。
  • focusfocusLast 会按深度优先方式搜索嵌套子节点中的可聚焦元素,不同于事件和观察器方法,它们只作用于一级宿主子节点。
  • observeUsing 不适用于文本节点。如果 Fragment 只包含文本子节点,React 会在开发环境中记录警告。
  • React 不会将通过 addEventListener 添加的事件监听器应用到隐藏的 <Activity> 树。当 Activity 边界从隐藏切换为可见时,监听器会自动应用。
  • 带有 ref 的 Fragment 的每个一级 DOM 子节点都会获得一个 reactFragments 属性——一个包含所有拥有该元素的 Fragment 实例的 Set<FragmentInstance>。这使得可以在多个 Fragment 之间缓存共享的观察器。缓存全局 IntersectionObserver

用法

返回多个元素

使用 Fragment,或等价的 <>...</> 语法,将多个元素分组在一起。你可以在任何只能放单个元素的位置使用它来放置多个元素。例如,组件只能返回一个元素,但借助 Fragment,你可以将多个元素组合在一起,然后作为一个整体返回:

function Post() {
return (
<>
<PostTitle />
<PostBody />
</>
);
}

Fragment 很有用,因为使用 Fragment 对元素分组不会影响布局或样式,而如果你把这些元素包裹在另一个容器中(例如 DOM 元素)就会有影响。如果你使用浏览器工具检查这个示例,你会看到所有 <h1><article> DOM 节点都作为兄弟节点出现,而周围没有包装器:

export default function Blog() {
  return (
    <>
      <Post title="An update" body="It's been a while since I posted..." />
      <Post title="My new blog" body="I am starting a new blog!" />
    </>
  )
}

function Post({ title, body }) {
  return (
    <>
      <PostTitle title={title} />
      <PostBody body={body} />
    </>
  );
}

function PostTitle({ title }) {
  return <h1>{title}</h1>
}

function PostBody({ body }) {
  return (
    <article>
      <p>{body}</p>
    </article>
  );
}

Deep Dive

如何在不使用特殊语法的情况下编写 Fragment?

上面的示例等价于从 React 导入 Fragment

import { Fragment } from 'react';

function Post() {
return (
<Fragment>
<PostTitle />
<PostBody />
</Fragment>
);
}

通常你不需要这样做,除非你需要 向你的 Fragment 传递 key


将多个元素赋值给变量

和其他元素一样,你可以将 Fragment 元素赋值给变量、将它们作为 props 传递,等等:

function CloseDialog() {
const buttons = (
<>
<OKButton />
<CancelButton />
</>
);
return (
<AlertDialog buttons={buttons}>
你确定要离开这个页面吗?
</AlertDialog>
);
}

将元素与文本分组

你可以使用 Fragment 将文本与组件组合在一起:

function DateRangePicker({ start, end }) {
return (
<>

<DatePicker date={start} />

<DatePicker date={end} />
</>
);
}

渲染 Fragment 列表

这里有一种情况,你需要显式编写 Fragment,而不是使用 <></> 语法。当你在循环中 渲染多个元素 时,需要为每个元素分配一个 key。如果循环中的元素是 Fragments,则需要使用普通的 JSX 元素语法,以便提供 key 属性:

function Blog() {
return posts.map(post =>
<Fragment key={post.id}>
<PostTitle title={post.title} />
<PostBody body={post.body} />
</Fragment>
);
}

你可以检查 DOM,以验证 Fragment 子节点周围没有包装元素:

import { Fragment } from 'react';

const posts = [
  { id: 1, title: 'An update', body: "It's been a while since I posted..." },
  { id: 2, title: 'My new blog', body: 'I am starting a new blog!' }
];

export default function Blog() {
  return posts.map(post =>
    <Fragment key={post.id}>
      <PostTitle title={post.title} />
      <PostBody body={post.body} />
    </Fragment>
  );
}

function PostTitle({ title }) {
  return <h1>{title}</h1>
}

function PostBody({ body }) {
  return (
    <article>
      <p>{body}</p>
    </article>
  );
}


Canary only 在没有包装元素的情况下添加事件监听器

Fragment ref 允许你在不添加包装 DOM 节点的情况下,给一组元素添加事件监听器。使用 ref 回调 来挂载并清理监听器:

import { Fragment, useState, useRef, useEffect } from 'react';

function ClickableFragment({ children, onClick }) {
  const fragmentRef = useRef(null);
  useEffect(() => {
    const fragmentInstance = fragmentRef.current;
    if (fragmentInstance === null) {
      return;
    }
    fragmentInstance.addEventListener('click', onClick);
    return () => {
      fragmentInstance.removeEventListener(
        'click',
        onClick
      );
    };
  }, [onClick])
  return (
    <Fragment ref={fragmentRef}>
      {children}
    </Fragment>
  );
}

export default function App() {
  const [clicks, setClicks] = useState(0);

  return (
    <>
      <p>总点击次数:{clicks}</p>
      <ClickableFragment onClick={() => {
        setClicks(c => c + 1);
      }}>
        <button>按钮 A</button>
        <button>按钮 B</button>
        <button>按钮 C</button>
      </ClickableFragment>
    </>
  );
}

addEventListener 调用会将监听器应用到 Fragment 的每个第一层 DOM 子节点。当前动态添加或移除子节点时,FragmentInstance 会自动添加或移除该监听器。

Deep Dive

Fragment ref 会指向哪些子元素?

FragmentInstance 会指向 Fragment 的第一层宿主(DOM)子节点。考虑这棵树:

<Fragment ref={ref}>
<div id="A" />
<Wrapper>
<div id="B">
<div id="C" />
</div>
</Wrapper>
<div id="D" />
</Fragment>

Wrapper 是一个 React 组件,所以 FragmentInstance 会穿过它去查找 DOM 节点。被指向的子节点是 ABDC 不会被指向,因为它嵌套在 DOM 元素 B 内部。

addEventListenerobserveUsinggetClientRects 这样的方法,作用于这些第一层 DOM 子节点。focusfocusLast 则不同——它们会以深度优先的方式搜索所有嵌套子节点,以找到可聚焦的元素。


Canary only 在一组元素之间管理焦点

Fragment ref 提供了 focusfocusLastblur 方法,可作用于 Fragment 内的所有 DOM 节点:

import { Fragment, useRef } from 'react';

function FormFields({ children }) {
  const fragmentRef = useRef(null);

  return (
    <>
      <div className="buttons">
        <button onClick={() => {
          fragmentRef.current.focus();
        }}>
          聚焦第一个
        </button>
        <button onClick={() => {
          fragmentRef.current.focusLast();
        }}>
          聚焦最后一个
        </button>
        <button onClick={() => {
          fragmentRef.current.blur();
        }}>
          取消聚焦
        </button>
      </div>
      <Fragment ref={fragmentRef}>
        {children}
      </Fragment>
    </>
  );
}

// 即使这些输入框是深层嵌套的,
// focus() 也会按深度优先搜索来找到它们。
export default function App() {
  return (
    <FormFields>
      <fieldset>
        <legend>Shipping</legend>
        <label>
          Street: <input name="street" />
        </label>
        <label>
          City: <input name="city" />
        </label>
      </fieldset>
    </FormFields>
  );
}

调用 focus() 会聚焦 street 输入框——即使它嵌套在 <fieldset><label> 里面。focus() 会以深度优先的方式搜索所有嵌套子节点,而不只是 Fragment 的直接子节点。focusLast() 以相反的顺序执行相同的操作,而 blur() 会在当前聚焦的元素位于 Fragment 内时移除焦点。


Canary only 将一组元素滚动到视图中

使用 scrollIntoView 可以在不使用包装元素的情况下,将 Fragment 的子元素滚动到视图中。传入 true(或省略该参数)会将第一个子元素滚动到顶部。传入 false 会将最后一个子元素滚动到底部:

import { Fragment, useRef } from 'react';

function ScrollableSection({ children }) {
  const fragmentRef = useRef(null);

  return (
    <>
      <div className="buttons">
        <button onClick={() => {
          fragmentRef.current.scrollIntoView();
        }}>
          滚动到顶部
        </button>
        <button onClick={() => {
          fragmentRef.current.scrollIntoView(false);
        }}>
          滚动到底部
        </button>
      </div>
      <div className="container">
        <Fragment ref={fragmentRef}>
          {children}
        </Fragment>
      </div>
    </>
  );
}

const items = [];
for (let i = 1; i <= 25; i++) {
  items.push('项目 ' + i);
}

export default function App() {
  return (
    <ScrollableSection>
      <h3>部分开头</h3>
      {items.map((item) => (
        <p key={item}>{item}</p>
      ))}
      <h3>部分结尾</h3>
    </ScrollableSection>
  );
}


Canary only 在没有包装元素的情况下观察可见性

使用 observeUsingIntersectionObserver 附加到 Fragment 的所有一级 DOM 子元素上。这让你可以在不要求子组件暴露 ref 或添加包装元素的情况下跟踪可见性:

import {
  Fragment,
  useRef,
  useLayoutEffect,
  useState,
} from 'react';
import Card from './Card';

function VisibleGroup({ onVisibilityChange, children }) {
  const fragmentRef = useRef(null);

  useLayoutEffect(() => {
    const visibleElements = new Set();
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach(e => {
          if (e.isIntersecting) {
            visibleElements.add(e.target);
          } else {
            visibleElements.delete(e.target);
          }
        });
        onVisibilityChange(visibleElements.size > 0);
      }
    );
    const fragmentInstance = fragmentRef.current;
    fragmentInstance.observeUsing(observer);
    return () => {
      fragmentInstance.unobserveUsing(observer);
    };
  }, [onVisibilityChange]);

  return (
    <Fragment ref={fragmentRef}>
      {children}
    </Fragment>
  );
}

export default function App() {
  const [isVisible, setIsVisible] = useState(true);

  return (
    <div className={isVisible ? 'page visible' : 'page'}>
      <div className="filler">向下滚动</div>
      <VisibleGroup onVisibilityChange={setIsVisible}>
        <Card title="第一部分" />
        <Card title="第二部分" />
      </VisibleGroup>
      <div className="filler">向上滚动</div>
    </div>
  );
}


Canary only 缓存全局 IntersectionObserver

对于有许多 observer 的网站,一个常见的性能优化是:按配置共享一个 IntersectionObserver,并根据哪个元素发生交叉,将其条目路由到正确的回调。带有 ref 的 Fragment 通过 reactFragments 属性支持同样的模式。

带有 ref 的 Fragment 的每个一级 DOM 子元素都有一个 reactFragments 属性:一个包含该元素的 FragmentInstance 对象的 Set。当共享的 observer 触发时,你可以使用这个属性查找哪个 FragmentInstance 拥有发生交叉的元素,并执行正确的回调。

import { useState, useCallback } from 'react';
import ObservedGroup from './ObservedGroup';
import Card from './Card';

export default function App() {
  const [bgColor, setBgColor] = useState(null);

  const onGreen = useCallback((entry) => {
    if (entry.isIntersecting) {
      setBgColor('#d4edda');
    }
  }, []);

  const onBlue = useCallback((entry) => {
    if (entry.isIntersecting) {
      setBgColor('#cce5ff');
    }
  }, []);

  return (
    <div className="page" style={{
      background: bgColor || 'white',
    }}>
      <div className="filler">向下滚动</div>
      <ObservedGroup onIntersection={onGreen}>
        <Card title="绿色区域" className="green" />
      </ObservedGroup>
      <div className="filler" />
      <ObservedGroup onIntersection={onBlue}>
        <Card title="蓝色区域" className="blue" />
      </ObservedGroup>
      <div className="filler">向上滚动</div>
    </div>
  );
}

多个使用相同选项的 ObservedGroup 组件会复用一个 IntersectionObserver。当任一区域滚动进入视野时,共享的 observer 会触发,并使用 reactFragments 将条目路由到正确的回调。