` 有时可行,但它也可能干扰组件的样式或布局。此外,如果组件没有暴露 `ref` 属性,你就需要修改该组件来实现这一点——而如果它来自一个你无法控制的库,这可能根本做不到。
+
+Fragment Refs 通过提供一组有限的常用 DOM 方法解决了这些问题,这些方法可以作用于任何 React 组件,无论它渲染什么内容。
+
+在 19.3 中,你可以通过直接向 [`
`](/reference/react/Fragment) 传递 ref 来使用它们。这个 ref 会给你一个 `FragmentInstance`,你可以用它来操作 Fragment 的 DOM 子元素:
+
+```js {2,5-6,10}
+function Component() {
+ const fragmentRef = useRef(null);
+
+ useEffect(() => {
+ const fragmentInstance = fragmentRef.current;
+ fragmentInstance.focus();
+ }, []);
+
+ return (
+
+ {posts.map(post => (
+
+ {post.title}
+
+ ))}
+
+ )
+}
+```
+
+`FragmentInstance` 会_作为一个整体_来操作子元素的 DOM,而不会改变其结构:
+
+- `addEventListener`、`removeEventListener` 和 `dispatchEvent` 管理一级子元素的事件。
+- `focus`、`focusLast` 和 `blur` 以深度优先的方式在嵌套的子元素间移动焦点。
+- `observeUsing` 和 `unobserveUsing` 用于连接 `IntersectionObserver` 或 `ResizeObserver`。
+- `getClientRects`、`getRootNode`、`compareDocumentPosition` 和 `scrollIntoView` 让你能够测量并滚动到 Fragment 的一级子元素。
+
+这样,Fragment Refs 让你能够为其他组件附加行为,而无需修改这些组件的内部实现,也不会改变它们已经产出的 DOM 结构。
+
+这个示例展示了一个带有 `onChange` 属性的 `InView` 组件,每当其子元素进入或离开视口时,该属性就会被触发:
+
+
+
+```js src/App.js active
+import { useState } from 'react';
+import Card from './Card';
+import InView from './InView';
+
+export default function App() {
+ const [isVisible, setIsVisible] = useState(true);
+
+ return (
+
+
Scroll down
+
+
+
+
+
+
+
Scroll up
+
+ );
+}
+```
+
+```js src/Card.js
+export default function Card({ title }) {
+ return {title}
;
+}
+```
+
+```js src/InView.js
+import {
+ Fragment,
+ useRef,
+ useLayoutEffect,
+} from 'react';
+
+export default function InView({ onChange, 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);
+ }
+ });
+ onChange(visibleElements.size > 0);
+ }
+ );
+ const fragmentInstance = fragmentRef.current;
+ fragmentInstance.observeUsing(observer);
+ return () => {
+ fragmentInstance.unobserveUsing(observer);
+ };
+ }, [onChange]);
+
+ return (
+
+ {children}
+
+ );
+}
+```
+
+```css
+.page {
+ transition: background 0.3s;
+}
+
+.page.visible {
+ background: #d4edda;
+}
+
+.filler {
+ height: 500px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #aaa;
+ font-size: 14px;
+}
+
+.card {
+ padding: 16px;
+ background: white;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ margin: 8px 16px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.08);
+ font-weight: 600;
+ font-size: 14px;
+}
+```
+
+
+```json package.json hidden
+{
+ "dependencies": {
+ "react": "19.3.0-canary-f1f7ed2a-20260904",
+ "react-dom": "19.3.0-canary-f1f7ed2a-20260904",
+ "react-scripts": "latest"
+ }
+}
+```
+
+
+
+请注意,`InView` 能够为它的子元素附加行为,即使这些子元素没有统一的父 DOM 元素,而且 `Card` 也没有暴露 `ref` 属性。
+
+关于使用 Fragment Refs 的更多信息,请参阅 [`` 文档](/reference/react/Fragment)。
+
+---
+
+## React DOM 新特性 {/*new-react-dom-features*/}
+
+### `browser` {/*browser*/}
+
+如果你的应用使用了服务端渲染,你的组件会在两种不同的环境中渲染:
+
+- 在服务器上,组件负责渲染生成初始 HTML
+- 在客户端上,组件负责为这些 HTML 附加事件处理器
+
+大多数时候,你的组件应该能够生成与其客户端初次渲染输出相匹配的 HTML,从而确保正确完成 hydration,同时让用户在初次加载时尽可能看到更多内容。
+
+但在少数情况下,组件可能无法在服务器上生成有意义的 UI。例如,它可能依赖 `localStorage` 这样仅在浏览器中可用的 API,或者需要读取浏览器的本地时区。在这些情况下,你可能希望让该组件完全退出服务端渲染。
+
+以前,你可能会在 Effect 中更新某个状态来实现这一点,或者通过检查 `window` 等浏览器 API 是否存在来判断:
+
+```js
+function Component() {
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true)
+ }, [])
+
+ // ...
+}
+
+function Component() {
+ const isBrowser = typeof window !== 'undefined';
+
+ // ...
+}
+```
+
+在 19.3 中,React 为这种做法提供了一个一等公民(first-class)API。
+
+组件可以调用 `use(browser())` 来退出服务端渲染:
+
+```js {5}
+import { use } from 'react';
+import { browser } from 'react-dom';
+
+function Component() {
+ use(browser());
+
+ // ...
+}
+```
+
+这会在服务器上触发 Suspense,但在客户端_不会_。在服务端渲染期间,最近的 Suspense 边界的后备方案会显示在 HTML 中。一旦组件在客户端完成 hydration,`use(browser())` 就不会挂起,组件可以继续正常渲染。
+
+下面是一个渲染设备本地时区的组件示例。按下 **重新加载**,可以看到初始 HTML 以及随后 React 在客户端的首次渲染:
+
+
+
+```js
+import { Suspense, use } from 'react';
+import { browser } from 'react-dom';
+
+function TimeZone() {
+ use(browser());
+ const timeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+ return {timeZone}
+}
+
+export default function App() {
+ return (
+ <>
+ Your current time zone is:
+
+
+
+ >
+ );
+}
+```
+
+
+```js src/Document.js hidden
+import App from './App.js';
+
+export default function Document() {
+ return (
+
+
+ Event details
+
+
+
+
+
+
+ );
+}
+```
+
+```js src/index.js hidden
+import { hydrateRoot } from 'react-dom/client';
+import { renderToReadableStream } from 'react-dom/server';
+import Document from './Document.js';
+import { flushReadableStreamToFrame } from './demo-helpers.js';
+import './styles.css';
+
+async function main(frame) {
+ const stream = await renderToReadableStream();
+ await flushReadableStreamToFrame(stream, frame);
+
+ // Wait so both the fallback and hydrated content are visible.
+ await new Promise(resolve => setTimeout(resolve, 1200));
+ hydrateRoot(frame.contentDocument, );
+}
+
+main(document.getElementById('preview'));
+```
+
+```js src/demo-helpers.js hidden
+export async function flushReadableStreamToFrame(readable, frame) {
+ const doc = frame.contentWindow.document;
+ const decoder = new TextDecoder();
+ const reader = readable.getReader();
+
+ while (true) {
+ const {done, value} = await reader.read();
+ if (done) {
+ break;
+ }
+ doc.write(decoder.decode(value, {stream: true}));
+ }
+
+ doc.write(decoder.decode());
+ doc.close();
+}
+```
+
+```html public/index.html hidden
+
+
+
+
+ Conditional browser rendering
+
+
+
+
+
+```
+
+```css src/styles.css hidden
+iframe {
+ width: 100%;
+ height: 240px;
+ border: 0;
+}
+```
+
+```json package.json hidden
+{
+ "dependencies": {
+ "react": "19.3.0-canary-f1f7ed2a-20260904",
+ "react-dom": "19.3.0-canary-f1f7ed2a-20260904",
+ "react-scripts": "latest"
+ }
+}
+```
+
+
+
+由于 TimeZone 在服务器上挂起,初始 HTML 中会包含 Suspense 的后备方案。经过一段人为的短暂延迟后,React 会对页面进行 hydration,让组件在浏览器中正常渲染。
+
+因此,对于无法在服务端渲染期间生成有意义 UI 的组件,`browser` 让你能够使用 Suspense 来呈现它们的加载状态,使它们可以与其他挂起直到就绪才渲染的组件协同工作。
+
+---
+
+与其他 `use` 调用一样,`use(browser())` 可以在条件语句中或提前返回(early return)之后调用。这让你能够编写根据某个条件(例如 prop 的值)退出服务端渲染的组件或自定义 Hook。
+
+下面是上面的同一个示例,只不过现在我们的 TimeZone 组件接受一个可选的默认值,可以将它作为初始 HTML 的一部分渲染出来:
+
+
+
+```js
+import { Suspense, use } from 'react';
+import { browser } from 'react-dom';
+
+function TimeZone({ defaultValue }) {
+ if (defaultValue) {
+ return {defaultValue}
;
+ }
+
+ use(browser());
+ const localTimeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+ return {localTimeZone}
+}
+
+export default function App() {
+ return (
+ <>
+
+
The event's time zone is:
+
+
+
+
+
+
+
Your current time zone is:
+
+
+
+
+ >
+ );
+}
+```
+
+
+```js src/Document.js hidden
+import App from './App.js';
+
+export default function Document() {
+ return (
+
+
+ Event details
+
+
+
+
+
+
+ );
+}
+```
+
+```js src/index.js hidden
+import { hydrateRoot } from 'react-dom/client';
+import { renderToReadableStream } from 'react-dom/server';
+import Document from './Document.js';
+import { flushReadableStreamToFrame } from './demo-helpers.js';
+import './styles.css';
+
+async function main(frame) {
+ const stream = await renderToReadableStream();
+ await flushReadableStreamToFrame(stream, frame);
+
+ // Wait so both the fallback and hydrated content are visible.
+ await new Promise(resolve => setTimeout(resolve, 1200));
+ hydrateRoot(frame.contentDocument, );
+}
+
+main(document.getElementById('preview'));
+```
+
+```js src/demo-helpers.js hidden
+export async function flushReadableStreamToFrame(readable, frame) {
+ const doc = frame.contentWindow.document;
+ const decoder = new TextDecoder();
+ const reader = readable.getReader();
+
+ while (true) {
+ const {done, value} = await reader.read();
+ if (done) {
+ break;
+ }
+ doc.write(decoder.decode(value, {stream: true}));
+ }
+
+ doc.write(decoder.decode());
+ doc.close();
+}
+```
+
+```html public/index.html hidden
+
+
+
+
+ Conditional browser rendering
+
+
+
+
+
+```
+
+```css src/styles.css hidden
+iframe {
+ width: 100%;
+ height: 240px;
+ border: 0;
+}
+```
+
+```json package.json hidden
+{
+ "dependencies": {
+ "react": "19.3.0-canary-f1f7ed2a-20260904",
+ "react-dom": "19.3.0-canary-f1f7ed2a-20260904",
+ "react-scripts": "latest"
+ }
+}
+```
+
+
+
+请注意,TimeZone 只在第二种情况下(即没有提供默认值时)才会挂起。
+
+这种模式的另一个实用示例是:让 `useQuery` 这样的数据请求 Hook 退出服务端渲染,除非传入了该查询的初始数据(例如来自服务端组件或框架的 loader 函数):
+
+```js {3}
+function useBrowserQuery(query, options) {
+ if (options.initialData === undefined) {
+ use(browser());
+ }
+
+ return useQuery(query, options);
+}
+
+function ProductDetails({ productId, initialData }) {
+ const product = useBrowserQuery(`/api/products/${productId}`, {
+ initialData,
+ });
+
+ return {product.name}
;
+}
+```
+
+现在,只要 ProductDetails 组件在服务端渲染期间收到了 `initialData`,它就可以被包含在 HTML 中。如果没有收到,它会一直挂起,直到在浏览器中渲染——届时 `useQuery` 就可以照常获取数据或从缓存中读取数据。
+
+关于 `browser` 的更多信息,请[查阅文档](/reference/react-dom/browser)。
+
+---
+
+### Trusted Types 支持 {/*trusted-types-support*/}
+
+React 19.3 与浏览器的 [Trusted Types API](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API) 集成,这是一项有助于防范基于 DOM 的 XSS 攻击的安全特性。当站点通过 `Content-Security-Policy: require-trusted-types-for 'script'` 强制启用 Trusted Types 时,浏览器会要求传递给 `innerHTML` 等注入点(injection sink)的值必须是通过你的净化策略创建的类型化对象(`TrustedHTML`、`TrustedScript`、`TrustedScriptURL`),而不是原始字符串。
+
+以前,React 在将值传递给 DOM API 之前总是先将其强制转换为字符串(通过 `'' + value`),这会把 Trusted Types 对象变回浏览器会拒绝的普通字符串。现在 React 会将这些值原样传递,不再进行强制转换,因此浏览器可以对它们进行验证,你的 Trusted Types 策略也能按预期工作。
+
+---
+
+## React 服务端组件新特性 {/*new-react-server-components-features*/}
+
+### `` 可以直接在服务端组件中渲染 {/*context-can-be-rendered-directly-in-server-components*/}
+
+虽然服务端组件不能_创建_ Context,但它们可以通过从 `'use client'` 模块导入来_渲染_ Context。
+
+以前,这需要客户端模块导出一个单独的包装组件,通常称为 Provider:
+
+```js {7-9}
+// user-context.js
+'use client';
+import { createContext } from 'react';
+
+export const UserContext = createContext(null);
+
+export function UserProvider({ currentUser, children }) {
+ return {children};
+}
+```
+
+```js {8}
+// server-component.js
+import { UserProvider } from './user-context';
+
+export async function Layout({ children }) {
+ const currentUser = await getCurrentUser();
+
+ return (
+
+ {children}
+
+ )
+}
+```
+
+请注意,在这个示例中,这个 Provider 唯一做的事情,就是把来自服务端组件的 prop 直接传递给 Context。
+
+在 React 19.3 中,服务端组件可以直接从 `'use client'` 模块导入并渲染 Context,无需额外的包装组件:
+
+```js {5}
+// user-context.js
+'use client';
+import { createContext } from 'react';
+
+export const UserContext = createContext(null);
+```
+
+```js {8}
+// server-component.js
+import { UserContext } from './user-context';
+
+export async function Layout({ children }) {
+ const currentUser = await getCurrentUser();
+
+ return (
+
+ {children}
+
+ )
+}
+```
+
+这对于那些仅仅为了让服务端组件与客户端树的其他部分共享数据而存在的 Context 尤其有用。
+
+
+---
+
+## 更新日志 {/*changelog*/}
+
+其他值得注意的变更
+- `react`:独立渲染各个 Transition,而不是将它们纠缠在一次渲染中,这样缓慢的 Transition 不再阻塞不相关的 Transition [#37290](https://github.com/react/react/pull/37290)
+- `react-dom`:在 hydration 期间的 Strict Mode 下双重调用 Effect,与客户端渲染的根节点保持一致 [#35961](https://github.com/react/react/pull/35961)
+- `react`:当 `use` 在条件语句中被错误使用时添加警告 [#37104](https://github.com/react/react/pull/37104)
+- `react`:在 `useActionState` 的错误信息中将「form state」重命名为「action state」[#35790](https://github.com/react/react/pull/35790)
+- `react-dom`:添加对 `onFullscreenChange` 和 `onFullscreenError` 事件的支持 [#34621](https://github.com/react/react/pull/34621)
+- `react-dom`:添加对 `maskType` SVG 属性的支持 [#35921](https://github.com/react/react/pull/35921)
+- `react-dom`:支持模块资源的 `fetchPriority` [#36835](https://github.com/react/react/pull/36835)
+- `react-dom`:当 React 在 Server Action 之后自动重置表单时触发 `onReset` [#35176](https://github.com/react/react/pull/35176)
+- `react-dom`:在 `submit` 事件中包含 `submitter` [#35590](https://github.com/react/react/pull/35590)
+- `react-dom`:将 iframe 上的 `credentialless` 识别为布尔属性 [#36148](https://github.com/react/react/pull/36148)
+- `react-dom`:将来自 `resize` 事件的更新批量处理到下一帧 [#35117](https://github.com/react/react/pull/35117)
+- `react-server`:将 `Error.cause` [#35810](https://github.com/react/react/pull/35810) 和 `AggregateError.errors` [#36156](https://github.com/react/react/pull/36156) 传输到客户端
+- `react-server`:在 Flight 中添加对 `` 的支持 [#34697](https://github.com/react/react/pull/34697)
+
+重要的错误修复
+
+- `react`:修复 `useDeferredValue` 卡在旧值上的问题 [#36134](https://github.com/react/react/pull/36134)
+- `react`:修复 Context 传播到 Suspense 后备方案 [#36160](https://github.com/react/react/pull/36160) 以及穿过已挂起的 Suspense 边界 [#35839](https://github.com/react/react/pull/35839) 的问题
+- `react`:修复在隐藏树中更新已脱水的 Suspense 边界时的挂起问题 [#37135](https://github.com/react/react/pull/37135)
+- `react`:修复 `useSyncExternalStore` 遗漏 `` 树隐藏期间发生的 store 变更的问题 [#36947](https://github.com/react/react/pull/36947)
+- `react`:修复 `useEffectEvent` 在 `forwardRef` 和 `memo` 组件中读取最新值的问题 [#34831](https://github.com/react/react/pull/34831)
+- `react`:修复组件状态更新时表单状态被重置的问题 [#34075](https://github.com/react/react/pull/34075)
+- `react`:修复 `lazy`、`memo` 以及改变组件类型的编辑中的多个 Fast Refresh 错误 [#36965](https://github.com/react/react/pull/36965)、[#36964](https://github.com/react/react/pull/36964)、[#36963](https://github.com/react/react/pull/36963)、[#36950](https://github.com/react/react/pull/36950)
+- `react`:修复了当包含 `` 的 `` 从 `visible` 模式切换为 `hidden` 后,`` 仍被提升到 `` 的问题 [#34983](https://github.com/react/react/pull/34983)
+- `react`:不让错误从隐藏的 `` 中逃逸 [#35074](https://github.com/react/react/pull/35074)
+- `react`:隐藏渲染在隐藏 `` 内部的 portal 内容 [#35091](https://github.com/react/react/pull/35091)
+- `react`:不再在错误信息中引用内部类型 `` [#35763](https://github.com/react/react/pull/35763)
+- `react-dom`:修复委托事件和已聚焦元素的焦点问题 [#36010](https://github.com/react/react/pull/36010)
+- `react-dom`:按照 DOM 规范规范化捕获选项,修复 `FragmentInstance` 监听器泄漏 [#36047](https://github.com/react/react/pull/36047)
+- `react-dom`:修复 Mobile Safari 中 `` 的崩溃问题 [#35337](https://github.com/react/react/pull/35337)
+- `react-dom`:修复 `` 与 `SuspenseList` 一起使用时的崩溃问题 [#35520](https://github.com/react/react/pull/35520)
+- `react-dom`:更新 `type="number"` 输入框的 `defaultValue`,使其与其他输入类型保持一致 [#36980](https://github.com/react/react/pull/36980)
+- `react-dom`:当 `innerHTML` 未变化时不再设置它 [#36949](https://github.com/react/react/pull/36949)
+- `react-dom`:修复 `nonce` 属性上的 hydration 不匹配误报 [#37030](https://github.com/react/react/pull/37030)
+- `react-dom`:修复 `react-dom/server` 在 Deno 上挂起的问题 [#35235](https://github.com/react/react/pull/35235)
+- `react-server`:修复 `decodeReplyFromBusboy` 中丢失 `FormData` 条目的问题 [#36468](https://github.com/react/react/pull/36468)
+- `react-server`:修复深层异步链导致的栈溢出 [#35612](https://github.com/react/react/pull/35612) 以及调试信息指数增长导致的 `RangeError` [#37481](https://github.com/react/react/pull/37481)
+
+完整的变更列表请参阅[更新日志](https://github.com/react/react/blob/main/CHANGELOG.md)。
+
+---
+
+_感谢 [Sam Selikoff](https://x.com/samselikoff) 撰写本文,感谢 [Matt Carroll](https://mattcarrollcode.com/)、[Dan Abramov](https://bsky.app/profile/danabra.mov) 和 [Andrew Clark](https://x.com/acdlite) 审阅本文。_