编程 React 19.2 用起来要注意什么:Activity、useEffectEvent、部分预渲染与 SSR 变更

2026-10-05 00:04:01

React 19.2 用起来要注意什么:Activity、useEffectEvent、部分预渲染与 SSR 变更

React 19.2 已发布到 npm,是过去一年里的第三个版本(前两个是 2025 年 12 月的 React 19 和 6 月的 React 19.1)。官方发布说明:react.dev/blog/2025/10/01/react-19-2。

这一版的新增内容按官方分类:

  • React 新特性:``、useEffectEvent、cacheSignal、Performance Tracks
  • React DOM 新特性:Partial Pre-rendering
  • 值得注意的变更:SSR 的 Suspense 边界批处理;SSR 在 Node 上的 Web Streams 支持;eslint-plugin-react-hooks v6;useId 默认前缀变更
  • Changelog:react-dom hoistable styles 上的 nonce(#32461);react-dom 警告将有文本内容的 React owned node 作为 Container(#32774)

Activity

Activity 用来替代条件渲染组件分支:

// Before
{isVisible &&
}

// After

19.2 中 Activity 支持两种 mode:

  • hidden:隐藏 children,卸载 effect,并把所有更新延后到 React 没有其他工作可做时再处理。
  • visible:显示 children,挂载 effect,更新正常处理。

也就是说,隐藏部分可以预渲染并持续渲染,而不影响屏幕上可见内容的性能。

典型用法:渲染用户接下来大概率会导航到的隐藏部分,或者保存用户已经离开的部分的状态。数据、CSS、图片在后台加载,导航会变快;返回导航时输入框之类的状态得以保留。官方后续计划给 Activity 增加更多 mode。

useEffectEvent

useEffect 有个常见模式:把外部系统的事件通知给应用代码。例如 ChatRoom({roomId, theme}) 建立连接,在 'connected' 时调用 showNotification('Connected!', theme)。问题在于,这个"事件"里用到的任何值发生变化,都会让包裹它的 Effect 重新执行——theme 一变,聊天室就重连。roomId 变化重连说得通,theme 变化不该重连。多数人的做法是关掉 lint 规则、把依赖排除掉,但这样 linter 就再也帮不上忙,容易出现依赖过期的 bug。

useEffectEvent 把这段逻辑里"事件"的部分从触发它的 Effect 中拆出来:

const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // All dependencies declared (Effect Events aren't dependencies)

和 DOM 事件类似,Effect Event 总是能"看到"最新的 props 和 state。

坑与限制:

  • Effect Event 不要写进依赖数组。需要升级到 eslint-plugin-react-hooks@latest,否则 linter 会试图把它插入为依赖。
  • Effect Event 只能和"它的" Effect 声明在同一个组件或 Hook 里。这些限制由 linter 校验。

使用时机:只用于概念上属于 Effect 触发的"事件"的函数,而不是用户事件。不要什么都往 useEffectEvent 里包,也不要只是为了消掉 lint 报错而用它,那会引入 bug。详见文档 "Separating Events from Effects"。

cacheSignal

仅适用于 React Server Components。cacheSignal 让你知道 cache() 的生命周期何时结束:

import { cache, cacheSignal } from 'react';

const dedupedFetch = cache(fetch);

async function Component() {
await dedupedFetch(url, { signal: cacheSignal() });
}

当结果不再会被缓存使用时,可以据此清理或中止工作,具体包括三种情况:React 成功完成渲染、渲染被中止、渲染失败。

Performance Tracks

19.2 在 Chrome DevTools 性能面板里新增了一组自定义 track。

  • Scheduler ⚛ track:显示 React 在不同优先级下正在做什么,比如用户交互的 "blocking"、startTransition 内更新的 "transition"。每个 track 内可以看到工作类型、调度这次更新的 event、render 发生的时间;也能看到某次更新是否因为等待其他优先级而阻塞,或者 React 是否在等 paint 再继续。
  • Components ⚛ track:显示 React 正在 render 或执行 effect 的组件树。"Mount" 表示 children 挂载或 effect 挂载,"Blocked" 表示渲染因为让出给 React 之外的工作而阻塞。

Partial Pre-rendering

React DOM 的新能力:提前预渲染应用的一部分,之后再恢复渲染。把静态部分预渲染出来放到 CDN 上,然后 resume 渲染 shell,补上动态内容。

先带 AbortController 调用 prerender:

const { prelude, postponed } = await prerender(, { signal: controller.signal });
await savePostponedState(postponed);

把 prelude shell 返回给客户端,之后再调用 resume "恢复"成 SSR 流:

const postponed = await getPostponedState(request);
const resumeStream = await resume(, postponed);

或者调用 resumeAndPrerender 恢复成 SSG 用的静态 HTML:

const postponedState = await getPostponedState(request);
const { prelude } = await resumeAndPrerender(, postponedState);

新增 API:

  • react-dom/server:resume(Web Streams)、resumeToPipeableStream(Node Streams)
  • react-dom/static:resumeAndPrerender(Web Streams)、resumeAndPrerenderToNodeStream(Node Streams)

另外,prerender 系列 API 现在会返回 postpone state,供 resume 系列 API 使用。

SSR 的 Suspense 边界批处理

这一版修掉了一个行为 bug:Suspense 边界在客户端渲染和 SSR 流式渲染下的揭示方式不一致。从 19.2 起,React 会把服务端渲染的 Suspense 边界的揭示批量延迟一小段时间,让更多内容一起出现,行为与客户端渲染对齐。此前在流式 SSR 中,suspense 内容会立刻替换 fallback。这个修复也为 SSR 期间给 Suspense 支持 `` 做了准备。

React 用启发式判断来保证这种节流不影响核心 web vitals 和搜索排名。例如整页加载时间接近 2.5s(LCP 被视为 "good" 的时间)时,React 会停止批处理、立即揭示内容,避免因为节流而丢掉这个指标。

SSR:Node 上的 Web Streams 支持

以下 API 现在可以在 Node.js 中使用:renderToReadableStream、prerender、resume、resumeAndPrerender。

坑:在 Node.js 里做服务端渲染仍然优先用 Node Streams。Node 环境下依然强烈推荐 renderToPipeableStream、resumeToPipeableStream、prerenderToNodeStream、resumeAndPrerenderToNodeStream。原因是 Node Streams 在 Node 里比 Web Streams 快得多,而且 Web Streams 默认不支持压缩,很容易让用户莫名其妙享受不到流式渲染的好处。

eslint-plugin-react-hooks v6

eslint-plugin-react-hooks@latest 已发布,recommended preset 默认使用 flat config,由 React Compiler 驱动的新规则需要手动开启。要继续用旧配置,改成 recommended-legacy:

extends: ['plugin:react-hooks/recommended-legacy']

useId 默认前缀变更

19.2 把 useId 的默认前缀从 :r:(19.0.0)或 «r»(19.1.0)改为 _r_。原先选用 CSS 选择器不合法的特殊字符,是为了尽量避免和用户手写的 ID 冲突。但要支持 View Transitions,就得保证 useId 生成的 ID 对 view-transition-name 和 XML 1.0 名称合法。

Changelog

  • react-dom:hoistable styles 上的 nonce(#32461)
  • react-dom:警告将有文本内容的 React owned node 作为 Container(#32774)

完整发布说明见 react.dev/blog/2025/10/01/react-19-2。

复制全文 生成海报 React前端 React19 前端工程 SSR React Hooks

推荐文章

程序员茄子在线接单