
如何升级到 React 19
如何升级到 React 19
2024 年 4 月 25 日 作者:Ricky Hanlon
React 19 新增的改进需要一些破坏性变更,但我们已尽力让升级过程尽可能顺畅,并且预计这些变更不会影响大多数应用。
注意:React 18.3 也已发布
为了帮助大家更轻松地升级到 React 19,我们发布了与 18.2 完全相同的
react@18.3版本,但新增了对已弃用 API 以及 React 19 所需其他变更的警告。
我们建议先升级到 React 18.3,以便在升级到 React 19 之前识别潜在问题。
有关 18.3 的变更列表,请参阅发布说明。
在本文中,我们将指导你完成升级到 React 19 的步骤:
如果你想帮助我们测试 React 19,请按照本升级指南中的步骤操作,并报告遇到的问题。有关 React 19 新增功能的列表,请参阅 React 19 发布文章。
安装
注意:现在必须使用新的 JSX transform
我们在 2020 年引入了新的 JSX transform,以改善打包体积,并允许在不导入 React 的情况下使用 JSX。在 React 19 中,我们新增了更多改进,例如将 ref 作为普通 prop 使用,以及 JSX 速度提升,这些都需要新的 transform。
如果未启用新的 JSX transform,你将看到以下警告:
text错误:你的应用(或其依赖之一)正在使用过时的 JSX transform。请更新为现代 JSX transform 以获得更快的性能:https://react.dev/link/new-jsx-transform我们预计大多数应用不会受到影响,因为大多数环境已经启用了该 transform。如需手动升级的说明,请参阅公告文章。
安装最新版本的 React 和 React DOM:
bash
npm install --save-exact react@^19.0.0 react-dom@^19.0.0
或者,如果你使用 Yarn:
bash
yarn add --exact react@^19.0.0 react-dom@^19.0.0
如果你使用 TypeScript,还需要更新类型定义:
bash
npm install --save-exact @types/react@^19.0.0 @types/react-dom@^19.0.0
或者,如果你使用 Yarn:
bash
yarn add --exact @types/react@^19.0.0 @types/react-dom@^19.0.0
我们还提供了一个 codemod 用于最常见的替换。请参阅下方的 TypeScript 变更。
Codemods
为了帮助升级,我们与 codemod.com 团队合作发布了 codemod,这些 codemod 会自动将你的代码更新为 React 19 中的许多新 API 和模式。
所有 codemod 都可在 react-codemod 仓库 中获取,Codemod 团队也加入并协助维护这些 codemod。要运行这些 codemod,我们建议使用 codemod 命令而不是 react-codemod,因为它运行更快、能处理更复杂的代码迁移,并提供更好的 TypeScript 支持。
注意:运行所有 React 19 codemods
使用 React 19
codemod配方运行本指南中列出的所有 codemods:
bashnpx codemod@latest react/19/migration-recipe这将运行来自
react-codemod的以下 codemods:
replace-reactdom-renderreplace-string-refreplace-act-importreplace-use-form-stateprop-types-typescript这不包括 TypeScript 变更。请参阅下方的 TypeScript 变更。
包含 codemod 的变更会附有相应命令。
有关所有可用 codemods 的列表,请参阅 react-codemod 仓库。
破坏性变更
渲染期间的错误不再被重新抛出
在之前的 React 版本中,渲染期间抛出的错误会被捕获并重新抛出。在开发模式下,我们还会通过 console.error 记录日志,导致重复的错误日志。
在 React 19 中,我们改进了错误处理方式,通过不再重新抛出来减少重复:
- 未捕获的错误:未被错误边界捕获的错误会报告到
window.reportError。 - 已捕获的错误:被错误边界捕获的错误会报告到
console.error。
此变更不应影响大多数应用,但如果你生产环境中的错误报告依赖重新抛出的错误,你可能需要更新错误处理方式。为此,我们为 createRoot 和 hydrateRoot 添加了新的自定义错误处理方法:
js
const root = createRoot(container, {
onUncaughtError: (error, errorInfo) => {
// ...记录错误报告
},
onCaughtError: (error, errorInfo) => {
// ...记录错误报告
}
});
更多信息请参阅 createRoot 和 hydrateRoot 的文档。
移除了已弃用的 React API
移除:函数组件的 propTypes 和 defaultProps
PropTypes 在 2017 年 4 月(v15.5.0) 被弃用。
在 React 19 中,我们从 React 包中移除了 propTypes 检查,使用它们将被静默忽略。如果你正在使用 propTypes,我们建议迁移到 TypeScript 或其他类型检查方案。
我们还从函数组件中移除了 defaultProps,改用 ES6 默认参数。类组件将继续支持 defaultProps,因为没有对应的 ES6 替代方案。
js
// 之前
import PropTypes from 'prop-types';
function Heading({text}) {
return <h1>{text}</h1>;
}
Heading.propTypes = {
text: PropTypes.string,
};
Heading.defaultProps = {
text: 'Hello, world!',
};
ts
// 之后
interface Props {
text?: string;
}
function Heading({text = 'Hello, world!'}: Props) {
return <h1>{text}</h1>;
}
注意: 使用以下命令将
propTypes转换为 TypeScript:
bashnpx codemod@latest react/prop-types-typescript
移除:使用 contextTypes 和 getChildContext 的 Legacy Context
Legacy Context 在 2018 年 10 月(v16.6.0) 被弃用。
Legacy Context 仅适用于使用 contextTypes 和 getChildContext API 的类组件,并因存在容易被遗漏的细微 bug 而被 contextType 取代。在 React 19 中,我们移除了 Legacy Context,以使 React 更小、更快。
如果你仍在类组件中使用 Legacy Context,则需要迁移到新的 contextType API:
js
// 之前
import PropTypes from 'prop-types';
class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string.isRequired,
};
getChildContext() {
return { foo: 'bar' };
}
render() {
return <Child />;
}
}
class Child extends React.Component {
static contextTypes = {
foo: PropTypes.string.isRequired,
};
render() {
return <div>{this.context.foo}</div>;
}
}
js
// 之后
const FooContext = React.createContext();
class Parent extends React.Component {
render() {
return (
<FooContext value='bar'>
<Child />
</FooContext>
);
}
}
class Child extends React.Component {
static contextType = FooContext;
render() {
return <div>{this.context}</div>;
}
}
移除:字符串 refs
字符串 refs 在 2018 年 3 月(v16.3.0) 被弃用。
类组件支持字符串 refs,后来因存在多个缺陷 而被 ref 回调取代。在 React 19 中,我们移除了字符串 refs,以使 React 更简单、更易理解。
如果你仍在类组件中使用字符串 refs,则需要迁移到 ref 回调:
js
// 之前
class MyComponent extends React.Component {
componentDidMount() {
this.refs.input.focus();
}
render() {
return <input ref='input' />;
}
}
js
// 之后
class MyComponent extends React.Component {
componentDidMount() {
this.input.focus();
}
render() {
return <input ref={input => this.input = input} />;
}
}
注意: 使用以下命令将字符串 refs 迁移为
ref回调:
bashnpx codemod@latest react/19/replace-string-ref
移除:模块模式工厂
模块模式工厂在 2019 年 8 月(v16.9.0) 被弃用。
这种模式很少使用,支持它会使 React 变得略微更大、更慢。在 React 19 中,我们移除了对模块模式工厂的支持,你需要迁移到普通函数:
js
// 之前
function FactoryComponent() {
return { render() { return <div />; } }
}
js
// 之后
function FactoryComponent() {
return <div />;
}
移除:React.createFactory
createFactory 在 2020 年 2 月(v16.13.0) 被弃用。
在广泛支持 JSX 之前,使用 createFactory 很常见,但如今已很少使用,并且可以用 JSX 替代。在 React 19 中,我们移除了 createFactory,你需要迁移到 JSX:
js
// 之前
import { createFactory } from 'react';
const button = createFactory('button');
js
// 之后
const button = <button />;
移除:react-test-renderer/shallow
在 React 18 中,我们将 react-test-renderer/shallow 更新为重新导出 react-shallow-renderer。在 React 19 中,我们移除了 react-test-render/shallow,建议直接安装该包:
bash
npm install react-shallow-renderer --save-dev
diff
- import ShallowRenderer from 'react-test-renderer/shallow';
+ import ShallowRenderer from 'react-shallow-renderer';
请注意:请重新考虑浅渲染
浅渲染依赖 React 内部实现,可能会阻碍你未来的升级。我们建议将测试迁移到 @testing-library/react 或 @testing-library/react-native。
移除了已弃用的 React DOM API
移除:react-dom/test-utils
我们已将 act 从 react-dom/test-utils 移入 react 包:
text
错误:`ReactDOMTestUtils.act` 已弃用,请改用 `React.act`。请从 `react` 导入 `act`,而不是从 `react-dom/test-utils` 导入。有关更多信息,请参见 https://react.dev/warnings/react-dom-test-utils。
要修复此警告,你可以从 react 导入 act:
diff
- import {act} from 'react-dom/test-utils'
+ import {act} from 'react';
所有其他 test-utils 函数已被移除。这些工具并不常用,并且容易导致依赖组件和 React 的低层实现细节。在 React 19 中,调用这些函数将会报错,其导出将在未来版本中被移除。
有关替代方案,请参阅警告页面。
注意: 使用以下命令将
ReactDOMTestUtils.act迁移为React.act:
bashnpx codemod@latest react/19/replace-act-import
移除:ReactDOM.render
ReactDOM.render 在 2022 年 3 月(v18.0.0) 被弃用。在 React 19 中,我们移除了 ReactDOM.render,你需要迁移到使用 ReactDOM.createRoot:
js
// 之前
import {render} from 'react-dom';
render(<App />, document.getElementById('root'));
// 之后
import {createRoot} from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
注意: 使用以下命令将
ReactDOM.render迁移为ReactDOMClient.createRoot:
bashnpx codemod@latest react/19/replace-reactdom-render
移除:ReactDOM.hydrate
ReactDOM.hydrate 在 2022 年 3 月(v18.0.0) 被弃用。在 React 19 中,我们移除了 ReactDOM.hydrate,你需要迁移到使用 ReactDOM.hydrateRoot:
js
// 之前
import {hydrate} from 'react-dom';
hydrate(<App />, document.getElementById('root'));
// 之后
import {hydrateRoot} from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);
注意: 使用以下命令将
ReactDOM.hydrate迁移为ReactDOMClient.hydrateRoot:
bashnpx codemod@latest react/19/replace-reactdom-render
移除:unmountComponentAtNode
ReactDOM.unmountComponentAtNode 在 2022 年 3 月(v18.0.0) 被弃用。在 React 19 中,你需要迁移到使用 root.unmount():
js
// 之前
unmountComponentAtNode(document.getElementById('root'));
// 之后
root.unmount();
了解更多请参阅 createRoot 和 hydrateRoot 的 root.unmount() 文档。
注意: 使用以下命令将
unmountComponentAtNode迁移为root.unmount:
bashnpx codemod@latest react/19/replace-reactdom-render
移除:ReactDOM.findDOMNode
ReactDOM.findDOMNode 在 2018 年 10 月(v16.6.0) 被弃用。
我们移除 findDOMNode,因为它是一个遗留的逃生舱口,执行缓慢、重构时脆弱、只返回第一个子节点,并且破坏了抽象层级(更多信息请参阅此处)。你可以用 DOM refs 替换 ReactDOM.findDOMNode:
js
// 之前
import {findDOMNode} from 'react-dom';
function AutoselectingInput() {
useEffect(() => {
const input = findDOMNode(this);
input.select()
}, []);
return <input defaultValue="Hello" />;
}
js
// 之后
function AutoselectingInput() {
const ref = useRef(null);
useEffect(() => {
ref.current.select();
}, []);
return <input ref={ref} defaultValue="Hello" />
}
新弃用
弃用:element.ref
React 19 支持 ref 作为 prop,因此我们弃用 element.ref 以支持 element.props.ref。
访问 element.ref 将会给出警告:
text
错误:访问 element.ref 不再受支持。ref 现在是一个常规 prop。它将在未来版本中从 JSX Element 类型中移除。
弃用:react-test-renderer
我们弃用 react-test-renderer,因为它实现了自己的渲染器环境,与用户实际使用的环境不匹配,提倡测试实现细节,并且依赖对 React 内部机制的窥探。
测试渲染器是在存在更可行的测试策略(如 React Testing Library)之前创建的,我们现在建议改用现代化的测试库。
在 React 19 中,react-test-renderer 会记录弃用警告,并已切换到并发渲染。我们建议将测试迁移到 @testing-library/react 或 @testing-library/react-native,以获取现代化且得到良好支持的测试体验。
值得注意的变化
StrictMode 改进
React 19 包含多个针对 Strict Mode 的修复和改进。
在开发模式下,Strict Mode 进行双渲染时,useMemo 和 useCallback 将在第二次渲染时复用第一次渲染的 memoized 结果。已经兼容 Strict Mode 的组件不应察觉行为差异。
与所有 Strict Mode 行为一样,这些功能旨在开发过程中主动暴露组件中的 bug,以便你在问题影响生产环境之前修复。例如,在开发模式下,Strict Mode 会在初始挂载时双重调用 ref 回调函数,以模拟已挂载组件被 Suspense 回退替换时发生的情况。
Suspense 改进
在 React 19 中,当组件挂起时,React 会立即提交最近 Suspense 边界的回退,而不等待整个兄弟树渲染完成。回退提交后,React 会安排另一次渲染,以“预热”树中其余部分的懒请求:
之前,当一个组件挂起时,挂起的兄弟组件会被渲染,然后才提交回退。
在 React 19 中,当组件挂起时,会先提交回退,然后渲染挂起的兄弟组件。
这一变化意味着 Suspense 回退显示得更快,同时仍然会预热挂起树中的懒请求。
移除 UMD 构建
UMD 在过去被广泛用作无需构建步骤即可加载 React 的便捷方式。如今,已有现代替代方案可以在 HTML 文档中作为脚本加载模块。从 React 19 开始,React 将不再生成 UMD 构建,以减少测试和发布流程的复杂性。
要使用 script 标签加载 React 19,我们建议使用基于 ESM 的 CDN,例如 esm.sh。
html
<script type="module">
import React from "https://esm.sh/react@19/?dev"
import ReactDOMClient from "https://esm.sh/react-dom@19/client?dev"
...
</script>
依赖 React 内部实现的库可能会阻碍升级
此版本包含对 React 内部实现的更改,这些更改可能会影响那些忽略我们呼吁、仍在使用 SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED 等内部实现的库。这些更改是 React 19 功能落地所必需的,并且不会破坏遵循我们指南的库。
根据我们的版本策略,这些更新未被列为破坏性变更,我们也不会提供有关如何升级这些库的文档。建议你移除所有依赖内部实现的代码。
为了反映使用内部实现的影响,我们将 SECRET_INTERNALS 后缀重命名为:
_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE
未来我们将更积极地阻止从 React 访问内部实现,以阻止这种用法并确保用户不会被升级所阻碍。
TypeScript 变更
移除了已弃用的 TypeScript 类型
我们根据 React 19 中移除的 API 清理了 TypeScript 类型。部分移除的类型已移至更相关的包中,还有一些不再需要用于描述 React 的行为。
注意: 我们发布了
types-react-codemod来迁移大多数与类型相关的破坏性变更:
bashnpx types-react-codemod@latest preset-19 ./path-to-app如果你有大量对
element.props的不安全访问,可以运行这个额外的 codemod:
bashnpx types-react-codemod@latest react-element-default-any-props ./path-to-your-react-ts-files
请查看 types-react-codemod 获取支持的替换列表。如果你觉得缺少某个 codemod,可以在缺失的 React 19 codemod 列表 中追踪。
必须使用 ref 清理函数
此变更包含在 react-19 codemod 预设中,名称为 no-implicit-ref-callback-return。
由于引入了 ref 清理函数,现在 TypeScript 会拒绝从 ref 回调返回任何其他内容。通常的修复方法是停止使用隐式返回:
diff
- <div ref={current => (instance = current)} />
+ <div ref={current => {instance = current}} />
原始代码返回了 HTMLDivElement 的实例,TypeScript 无法知道这是清理函数还是其他内容。
useRef 需要参数
此变更包含在 react-19 codemod 预设中,名称为 refobject-defaults。
长期以来,TypeScript 和 React 配合使用时的一个痛点就是 useRef。我们修改了类型,使 useRef 现在必须传入参数。这显著简化了其类型签名,它的行为现在更接近 createContext。
ts
// @ts-expect-error: 应为 1 个参数,但实际为 0
useRef();
// 通过
useRef(undefined);
// @ts-expect-error: 应为 1 个参数,但实际为 0
createContext();
// 通过
createContext(undefined);
这也意味着所有 ref 都是可变的。你不再会遇到因为用 null 初始化 ref 而无法修改它的问题:
ts
const ref = useRef<number>(null);
// 无法赋值给 'current',因为它是只读属性
ref.current = 1;
MutableRef 现在已被弃用,取而代之的是单个 RefObject 类型,useRef 将始终返回该类型:
ts
interface RefObject<T> {
current: T
}
declare function useRef<T>: RefObject<T>
useRef 仍然有一个便捷重载:useRef<T>(null) 会自动返回 RefObject<T | null>。为了缓解 useRef 必须传参的迁移问题,我们添加了一个 useRef(undefined) 的便捷重载,它会自动返回 RefObject<T | undefined>。
关于此变更的先前讨论,请参阅 [RFC] 使所有 ref 可变。
ReactElement TypeScript 类型的变更
此变更包含在 react-element-default-any-props codemod 中。
如果元素类型被标记为 ReactElement,其 props 现在默认为 unknown,而不是 any。如果你向 ReactElement 传入类型参数,则不会受到影响:
ts
type Example2 = ReactElement<{ id: string }>["props"];
// ^? { id: string }
但如果你依赖默认行为,现在需要处理 unknown:
ts
type Example = ReactElement["props"];
// ^? 之前是 'any',现在是 'unknown'
如果你有大量依赖元素属性不安全访问的遗留代码,可能就需要进行这类处理。元素内省只是一个逃生舱口,你应该通过显式标注 any 来明确你的属性访问是不安全的。
TypeScript 中的 JSX 命名空间
此变更包含在 react-19 codemod 预设中,名称为 scoped-jsx。
用户长期以来的一个请求是,从类型中移除全局 JSX 命名空间,改而使用 React.JSX。这有助于防止污染全局类型,从而避免利用 JSX 的不同 UI 库之间产生冲突。
现在,你需要将 JSX 命名空间的模块扩充包裹在 declare module "...." 中:
diff
// global.d.ts
+ declare module "react" {
namespace JSX {
interface IntrinsicElements {
"my-element": {
myElementProps: string;
};
}
}
+ }
具体的模块标识符取决于你在 tsconfig.json 的 compilerOptions 中指定的 JSX runtime:
- 对于
"jsx": "react-jsx",应为react/jsx-runtime。 - 对于
"jsx": "react-jsxdev",应为react/jsx-dev-runtime。 - 对于
"jsx": "react"和"jsx": "preserve",应为react。
更好的 useReducer 类型
感谢 @mfp22,useReducer 现在有了更好的类型推断。
但是,这需要一项破坏性变更:useReducer 不再接受完整的 reducer 类型作为类型参数,而是要么不传类型参数(依赖上下文类型),要么同时传入 state 和 action 类型。
新的最佳实践是_不_向 useReducer 传递类型参数:
diff
- useReducer<React.Reducer<State, Action>>(reducer)
+ useReducer(reducer)
在边缘情况下,如果无法推断类型,可以通过元组显式传入 Action 类型来指定 state 和 action:
diff
- useReducer<React.Reducer<State, Action>>(reducer)
+ useReducer<State, [Action]>(reducer)
如果内联定义 reducer,我们建议为函数参数添加类型注解:
diff
- useReducer<React.Reducer<State, Action>>((state, action) => state)
+ useReducer((state: State, action: Action) => state)
如果你将 reducer 移到 useReducer 调用之外,也需要这样做:
ts
const reducer = (state: State, action: Action) => state;
更新日志
其他破坏性变更
- react-dom:对
src和href中的 javascript URL 报错 #26507 - react-dom:从
onRecoverableError中移除errorInfo.digest#28222 - react-dom:移除
unstable_flushControlled#26397 - react-dom:移除
unstable_createEventHandle#28271 - react-dom:移除
unstable_renderSubtreeIntoContainer#28271 - react-dom:移除
unstable_runWithPriority#28271 - react-is:从
react-is中移除已弃用的方法 #28224
其他值得注意的变化
- react:批量处理同步、默认和连续通道 #25700
- react:不再预渲染挂起组件的兄弟组件 #26380
- react:检测由渲染阶段更新引起的无限更新循环 #26625
- react-dom:popstate 中的过渡现在是同步的 #26025
- react-dom:移除 SSR 期间的 layout effect 警告 #26395
- react-dom:对
src/href(锚点标签除外)为空字符串时给出警告,且不会设置该属性 #28124
完整的变更列表,请参阅更新日志。
感谢 Andrew Clark、Eli White、Jack Pope、Jan Kassens、Josh Story、Matt Carroll、Noah Lemen、Sophie Alpert 和 Sebastian Silbermann 审阅和编辑本文。
帮助我们改进文档
发现翻译问题或内容错误?请告诉我们。
