React 16 一路升到 19

举报
yd_244592142 发表于 2026/10/28 09:34:40 2026/10/28
【摘要】 一、先别动 package.json:搞清楚你现在站在哪几乎所有失败的升级都源于同一个动作——先把版本号改了,再祈祷它能跑起来。动手前先跑一条命令,把现状摊开:npm ls react react-domgrep -rE "ReactDOM\.render|findDOMNode|defaultProps|propTypes|contextTypes|forwardRef" src/ --...

一、先别动 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 18
  • Support for defaultProps will be removed from function components
  • ... contains the string ref
  • ReactDOMTestUtils.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 组件都照常工作。我的建议是先让一切变绿,再按收益排序逐项采纳:

  1. ref as prop + Context as provider——纯减负,codemod 顺手就做了
  2. Actions / useActionState / useOptimistic——表单密集的后台系统收益最大,可以一个表单一个表单地迁
  3. 文档元数据(<title> / <meta> 就地渲染)——React 会自动提升到 <head>,能删掉一堆 react-helmet 之类的依赖
  4. View Transitions + Fragment Refs——这两个在 19.3 才转正,动画场景值得一看,但不急
  5. 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。出问题时能精确回退到任意一层。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。