React 18 项目升级到 React 19,怎样分步迁移并验证行为?
以含表单、弹窗和 SSR 的项目为例,解释版本与依赖检查、根节点 API、ref 回调清理、类型变化和升级后的行为验证,避免把安装成功当作迁移完成。
简答
先升级到 React 18.3,它会对 19 中被移除的 API 给出警告,再升级到 19。主要破坏性变更:移除 ReactDOM.render 和 hydrate、函数组件的 propTypes 和 defaultProps、string ref、legacy context,test-utils 的 act 改为从 react 导入;ref 回调可以返回清理函数。用官方 codemod 处理机械性改动,再跑测试、SSR 和关键交互验证。使用 RSC 时要升级到修复了 CVE-2025-55182 及后续漏洞的补丁版本。
一个 React 18 后台同时使用组件库、表单库和 SSR 框架。升级依赖后构建能通过,但弹窗打开时报旧 API 错误,输入框 ref 又出现类型错误。迁移涉及应用代码、第三方库和服务端构建,不能只把 package.json 中的版本号改完就结束。
这里讨论官方 React 18 → 19 的迁移边界,不代表所有 19.x 小版本行为相同。部署应选维护中的最新补丁版本。典型例子是 2025-12 披露的 CVE-2025-55182:react-server-dom-webpack/parcel/turbopack 的 19.0.0、19.1.0、19.1.1、19.2.0 存在未授权远程代码执行,首批修复为 19.0.1、19.1.2、19.2.1;之后又有拒绝服务和源码泄露的后续公告,官方建议直接升到各包最新版。不使用 Server Components 的纯客户端应用不受影响。
升级指南列出的主要破坏性变更:函数组件的 propTypes 和 defaultProps 被移除(改用 ES 默认参数),string ref 和 legacy context 被移除,react-dom/test-utils 的 act 改为从 react 导入,ReactDOM.render / hydrate 改为 createRoot / hydrateRoot。另外,canary 中的 useFormState 已改名为 useActionState;ref 可以直接作为 prop 传递,forwardRef 不再必需。
先让旧版本告诉你哪里需要修改
官方建议先升级到 React 18.3:它在 18.2 基础上增加针对弃用 API 和迁移事项的警告。先在旧主版本清除这些警告,可以把行为修改与主版本升级分开定位。
同时查看应用的 React、React DOM、类型包和框架支持矩阵。组件库的 peerDependencies 只表达声明支持范围,不证明所有交互已验证;反过来,也不要用强制安装绕过冲突后就忽略风险。检查依赖树中是否存在重复 React 副本,尤其是本地链接的组件库和工作区包。
保存当前锁文件与可部署产物,记录登录、编辑、提交、弹窗焦点和 SSR 导航的基线。此次迁移先保持产品行为一致,不同时引入一整套新表单架构,这样出现问题才容易定位。
根节点 API 决定应用怎样启动和销毁
React 19 删除了 ReactDOM.render、ReactDOM.hydrate 等旧入口。普通客户端页面使用 createRoot,已有服务端 HTML 则使用 hydrateRoot,不能为了消除错误把 SSR 页面改成清空后重新渲染。
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
const container = document.getElementById('root');
if (!container) throw new Error('缺少应用挂载容器');
const root = createRoot(container);
root.render(<App />);
// 仅供宿主真正移除整个应用时调用,不在每次普通路由切换时执行。
export function unmountApp() {
root.unmount();
}
迁移时搜索的不只是主入口,还包括测试工具、旧弹窗封装和微前端挂载器。由 SSR 框架管理的入口应遵循框架迁移说明,不自行创建第二个根节点。root.unmount 后若需要重新挂载,应创建新的 root。
ref 回调的返回值有了明确含义
React 19 支持 ref 回调返回清理函数。原来无意中返回 DOM 的箭头表达式可能因此出现类型错误,例如 ref={node => current = node}。如果只是保存引用,改成花括号块避免隐式返回;如果注册资源,就明确返回清理函数。
import { useCallback } from 'react';
export function MeasuredBox() {
const attach = useCallback((node) => {
if (!node) return;
const observer = new ResizeObserver(([entry]) => {
node.dataset.width = String(Math.round(entry.contentRect.width));
});
observer.observe(node);
return () => observer.disconnect();
}, []);
return <section ref={attach}>调整窗口可更新测量值</section>;
}
这是 React 19 的清理语义示例,不可原样当成 React 18 兼容实现。它只把测量值写入 data 属性,不用测量结果反复改变宽度,避免制造尺寸反馈循环。ResizeObserver 的目标环境仍需单独检查。
返回清理函数后,不应再假设卸载时一定靠传入 null 完成清理。开发 Strict Mode 的额外 ref 检查也可能暴露重复订阅问题。useCallback 保持函数身份只是减少不必要的拆装,不能替代正确释放资源。
类型错误要修复语义,不能批量压成 any
更新 React 类型包后,应检查 ref 返回值、useRef 调用、JSX 命名空间以及 ReactElement.props 的使用。若代码曾依赖宽松类型读取未知元素属性,应先缩小类型或改变组件接口;直接断言为 any 会把迁移问题留到运行时。
React 19 要求现代 JSX transform。确认构建工具实际生成的代码,而不是只看源码里是否写了 import React。官方 codemod 可辅助替换语法,但不能理解全部业务副作用;先固定工具版本,在可回退分支运行,审查差异后再进行验证。
怎样证明没有改变关键行为
首先运行类型和现有测试,再用生产构建验证以下真实路径:表单输入是否保留、提交是否只执行一次、弹窗关闭是否恢复焦点、路由切换是否清理订阅、错误边界是否展示恢复入口。
SSR 应检查首次 HTML、hydration 警告和客户端导航,特别是日期、ID 和客户端专属状态。开发 Strict Mode 与生产各有检查价值:开发中的额外执行用于发现副作用错误,不应通过关闭它来证明迁移成功。
升级前后的错误上报也要核对。React 19 改变了部分渲染错误的报告方式,不能只盯全局 error 监听器;按框架和根节点 API 文档配置捕获路径,确认不会漏报或重复上报。上传诊断数据时裁剪用户输入、表单值和敏感地址。
回滚需要完整发布单元
出现回归时应恢复兼容的源码、锁文件、服务端与客户端构建产物,而不只是把 react 版本改回去。若新版本已经使用 ref 清理函数等 19 专属行为,直接降依赖不能保证旧代码可运行。缓存中的旧 HTML 与新资源也需兼容,发布切换必须沿用项目既有的原子部署或版本化资源策略。
迁移成功的证据是目标版本下关键行为、错误监控和资源清理符合预期。包安装成功、警告减少或首屏能打开,都只能证明其中一部分。