React 16 一路升到 19
一、先别动 package.json:搞清楚你现在站在哪
几乎所有失败的升级都源于同一个动作——先把版本号改了,再祈祷它能跑起来。
动手前先跑一条命令,把现状摊开:
npm ls react react-dom
grep -rE "ReactDOM\.render|findDOMNode|defaultProps|propTypes|contextTypes|forwardRef" src/ --include="*.tsx" --include="*.jsx" -l | head -50
第二条命令列出来的文件数量,基本就是你的工作量。这里有个关键判断:React 19 的破坏性变更高度集中在"被废弃多年的老 API"上,现代函数组件几乎不受影响。所以如果你的项目是 2021 年之后写的、没有 class 组件、没有 enzyme,那恭喜你,这是个半天活;如果是 2018 年的老项目,那这是个需要立项的工程。
三条典型路线的难度差异极大:
| 起点 | 难度 | 主要障碍 |
|---|---|---|
| React 18 → 19 | 低(0.5–3 天) | 移除的旧 API、测试工具、类型定义 |
| React 17 → 19 | 中 | 先过 18 的并发渲染 + 上面的全套 |
| React 16 → 19 | 高 | Hooks 改造、StrictMode、生命周期、上面全套 |
二、为什么不建议你一步跳到 19
React 团队为这次升级专门发了一个"过渡版本":React 18.3 与 18.2 功能完全一致,唯一的区别是它会对所有 React 19 要移除的 API 打出废弃警告。
这是整个迁移里性价比最高的一步。因为 18.3 上这些代码还能跑,你是在一个正常运转的应用里改 bug,而不是在一堆红色报错里猜哪里坏了。
npm install react@18.3 react-dom@18.3
跑一遍应用,跑一遍测试,然后把控制台里所有警告清干净。你会看到类似这些:
ReactDOM.render is no longer supported in React 18Support for defaultProps will be removed from function components... contains the string refReactDOMTestUtils.act is deprecated in favor of React.act
这些警告全部清零之前,不要动大版本号。 这一步做扎实,后面的 19 升级会无聊到让人失望——这正是我们想要的。
同时确认两件事:一是项目用的是新版 JSX transform(React 19 强制要求,tsconfig 里是 "jsx": "react-jsx" 而非 "jsx": "react");二是 TypeScript 版本在 5.x 以上。
三、体检清单:三类会卡住你的东西
1. 第三方依赖的 peerDependencies
UI 库、表单库、路由、测试工具,这四类是重灾区。检查方法:
npm ls --depth=0 | xargs -I{} npm view {} peerDependencies 2>/dev/null | grep -B5 react
更实际的做法是直接 npm install react@19 看 npm 报不报 ERESOLVE。如果报冲突,先找该库有没有新版本,而不是加 --legacy-peer-deps——后者只是把警告藏起来,库本身不兼容的问题一个都没解决。
2. 构建工具:CRA 已经退场
如果你还在用 react-scripts,这次升级绕不开换构建工具。CRA 停更多年,官方"创建新项目"页面已不再列出它,Vite 是当前 SPA 的默认选择。好消息是这个迁移很机械:
npm uninstall react-scripts
npm install -D vite @vitejs/plugin-react
然后四件事:public/index.html 移到项目根目录并加上 <script type="module" src="/src/main.jsx">、删掉 %PUBLIC_URL%、环境变量 REACT_APP_* 改成 VITE_*、process.env.XXX 改成 import.meta.env.XXX。社区有 npx migrate-to-vite@latest cra 能自动完成大部分工作。
一个真实收益参考:某生产项目从 CRA 迁到 Vite 后,构建时间从 274 秒降到 46 秒。这种提速对老项目的日常迭代是质变。
3. 测试体系
enzyme 不支持 React 18 及以上,必须迁到 React Testing Library,这是很多人低估的工作量。另外 react-dom/test-utils 在 19 里只剩 act 一个导出,Simulate 之类全没了。
四、正式升级:两行命令 + 两个 codemod
npm install --save-exact react@^19.0.0 react-dom@^19.0.0
npm install --save-exact -D @types/react@^19.0.0 @types/react-dom@^19.0.0
react 和 react-dom 必须同版本,这条任何时候都成立。
然后跑官方 codemod,它会处理 ReactDOM.render → createRoot、字符串 ref、act 导入迁移、useFormState → useActionState 等机械改动:
npx codemod@latest react/19/migration-recipe
TypeScript 项目还要跑类型 codemod:
npx types-react-codemod@latest preset-19 ./src
务必在干净的 git 分支上跑,然后仔细 review diff。 codemod 擅长模式匹配,不擅长理解你的意图。
五、Breaking Change 逐个拆
1. 根 API:render → createRoot
// 之前
import ReactDOM from "react-dom";
ReactDOM.render(<App />, document.getElementById("root"));
// 之后
import { createRoot } from "react-dom/client";
const root = createRoot(document.getElementById("root"));
root.render(<App />);
配套:ReactDOM.hydrate → hydrateRoot(),unmountComponentAtNode(el) → root.unmount()。注意后者意味着你得把 root 存下来。
2. 字符串 ref:直接抛错
ref="input" 在 19 里会直接抛 Expected ref to be a function... 并卸载根节点。改成 createRef 或回调 ref。
3. findDOMNode:删了
换成在需要测量的 DOM 节点上挂 ref。
4. defaultProps:最阴险的一个
这是我认为最值得单独拎出来讲的一条。函数组件上的 defaultProps 在 React 19 里是静默失效——不报错、不警告,prop 直接变成 undefined。
// 之前
function Button({ size }) { /* ... */ }
Button.defaultProps = { size: "md" };
// 之后
function Button({ size = "md" }) { /* ... */ }
注意一个容易混淆的点:class 组件上的 defaultProps 依然有效,这是官方有意保留的。所以别全局一刀切。
5. propTypes 与 legacy context
propTypes 同样静默失效,不再做任何校验。TypeScript 项目本就不需要它;JS 项目要改成运行时 schema 校验(zod 之类)或干脆补类型。
legacy context(contextTypes / childContextTypes / getChildContext)彻底移除,this.context 会变成 {},改用 createContext。
6. ref 成为普通 prop,forwardRef 退休
这是 19 里最讨喜的改动:
// 之前
const Input = forwardRef((props, ref) => <input ref={ref} {...props} />);
// 之后
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
forwardRef 目前还能用,但 dev 环境会打废弃警告,未来会移除。第三方库内部还在用 forwardRef 的完全不受影响,不用等它们。
这里有个坑:既然 ref 现在是普通 prop,{...props} 展开时可能把 ref 无意间透传下去。所以要显式解构:const { ref, ...rest } = props。
7. ref 回调可以返回清理函数——也因此带来新坑
// 危险!箭头函数隐式返回了赋值结果
<div ref={el => (this.myEl = el)} />
// 正确
<div ref={el => { this.myEl = el; }} />
React 19 中 ref 回调的返回值会被当作清理函数处理,返回了非函数、非 undefined 的值就会告警。老项目里这种单行箭头写法非常常见,值得全局搜一遍。
8. Context 可以直接当 Provider
<ThemeContext.Provider> → <ThemeContext>,旧写法仍可用但已废弃。
9. 错误上报行为变化
被错误边界捕获的错误只 console.error 一次,未捕获的错误走 window.reportError;createRoot 新增 onCaughtError / onUncaughtError 让你自己接。如果你接了 Sentry 之类的前端监控,这里要顺一遍。
六、TypeScript:升级后最多的报错在这里
@types/react 19 收紧了一批类型,升级后 tsc --noEmit 大概率一片红。常见三类:ref 相关类型(用 React.Ref<T> 而不是 RefObject<T>,因为它现在是 RefCallback | RefObject | null 的联合)、ReactElement 的 props 默认从 any 变成更严格的类型、以及 useRef 需要显式初始值。
类型 codemod(types-react-codemod)能解决大半,剩下的手工改。先跑 codemod 再改类型,顺序反了会白干。
七、路由:最大的外部变量
如果你用 React Router,v7 是一次范式转变——它合并了 Remix,包从 react-router-dom 收敛到 react-router,并且要求 Node 20+。
关键在于不要直接跳版本。v6 提供了一组 future flags,可以让你逐个开启 v7 的行为,每开一个跑一次测试:
createBrowserRouter(routes, {
future: {
v7_normalizeFormMethod: true,
v7_relativeSplatPath: true,
v7_fetcherPersist: true,
v7_partialHydration: true,
v7_skipActionErrorRevalidation: true,
},
});
全部开启且测试通过后再升 v7——官方的说法是如果你开齐了所有 future flags,v7 升级就没有破坏性变更。每开一个 flag 提交一次,别攒着一起上。
八、升级之后:新特性别一次全上
升到 19 之后,没有任何东西强制你使用新特性。现有组件、hooks、class 组件都照常工作。我的建议是先让一切变绿,再按收益排序逐项采纳:
- ref as prop + Context as provider——纯减负,codemod 顺手就做了
- Actions / useActionState / useOptimistic——表单密集的后台系统收益最大,可以一个表单一个表单地迁
- 文档元数据(
<title>/<meta>就地渲染)——React 会自动提升到<head>,能删掉一堆 react-helmet 之类的依赖 - View Transitions + Fragment Refs——这两个在 19.3 才转正,动画场景值得一看,但不急
- React Compiler——自动记忆化,可以开始评估,但别指望它替你重构
截止写作时,React 稳定版是 19.3.x。19.3 是增量版本,没有移除任何东西,所以升级到 19.0 之后,后续的小版本跟进成本很低。
九、验收与回滚
验收清单:
npx tsc --noEmit # 类型
npm test -- --watch=false # 测试
rg "forwardRef|Context\.Provider|defaultProps|propTypes|findDOMNode" src/ # 残留
除了自动化测试,还要人工过一遍:Strict Mode 保持开启(19.3 起 hydration 阶段也会双调用 effect,effect 必须幂等)、记录升级前后的 LCP/INP 基线、控制台零新增警告。
回滚策略很简单,但要在动手前就约定好:版本升级、codemod、手工修复分成三个独立 commit。出问题时能精确回退到任意一层。
- 点赞
- 收藏
- 关注作者
评论(0)