原文:https://react.dev/blog/2024/04/25/react-19-upgrade-guide
作者:Ricky Hanlon
译者:ChatGPT 4 Turbo
注意
此 beta 版本是为了让库准备迁移到 React 19。应用开发者应该升级到 18.3.0,并等待 React 19 的稳定版本,因为我们正在与库合作并根据反馈进行更改。
React 19 加入的改进需要一些重大更改,但我们努力使升级尽可能顺畅,我们预计这些更改不会影响大多数应用程序。
为了帮助简化升级过程,今天我们还发布了 React 18.3。
注意
也已发布 React 18.3
为了使升级到 React 19 更加容易,我们发布了一个 react@18.3
版本,它与 18.2 相同,但增加了对已弃用 API 和其他为 React 19 准备的更改的警告。
我们建议首先升级到 React 18.3,以帮助识别在升级到 React 19 之前可能存在的任何问题。
有关 18.3 中更改的列表,请参阅发布说明。
在这篇文章中,我们将指导你完成将库升级到 React 19 beta 的步骤:
如果你想帮助我们测试 React 19,请按照此升级指南中的步骤操作,并报告你遇到的任何问题。查看 React 19 beta 新增功能的列表,请参阅 React 19 发布文章。
安装
注意
现在需要新的 JSX 转换
我们在 2020 年引入了新的 JSX 转换,以改善包大小并无需导入 React 就能使用 JSX。在 React 19 中,我们增加了像将 ref 作为 prop 和 JSX 速度提升这样的额外改进,这需要新的转换。
如果新的变换没有启用,你会看到这个警告:
我们预计大多数应用不会受到影响,因为在大多数环境中这个变换已经被启用。有关如何升级的手动说明,请参见公告帖子。
安装最新版本的 React 和 React DOM:
npm install react@beta react-dom@beta
如果你在使用 TypeScript,你还需要更新类型定义。一旦 React 19 作为稳定版发布,你可以像往常一样从 @types/react
和 @types/react-dom
安装类型定义。在测试期间,类型定义在不同的包中,需要在你的 package.json
中强制使用:
{
"dependencies": {
"@types/react": "npm:types-react@alpha",
"@types/react-dom": "npm:types-react-dom@alpha"
},
"overrides": {
"@types/react": "npm:types-react@alpha",
"@types/react-dom": "npm:types-react-dom@alpha"
}
}
我们还提供了一个 codemod,用于最常见的替换。下面见 TypeScript 变动。
破坏性变动
渲染中的错误不会被重新抛出
在之前的 React 版本中,渲染过程中抛出的错误会被捕获并重新抛出。在开发环境下,我们还会记录到 console.error
,导致错误日志重复。
在 React 19 中,我们改进了错误处理的方式,通过不重新抛出来减少重复:
- 未捕获的错误:没有被错误边界捕获的错误会报告给
window.reportError
。 - 捕获的错误:被错误边界捕获的错误会报告给
console.error
。
这个变化不应该影响大多数应用,但是如果你的生产环境错误报告依赖于错误被重新抛出,你可能需要更新你的错误处理方式。为了支持这一点,我们在 createRoot
和 hydrateRoot
中添加了新的方法,用于自定义错误处理:
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 包中移除了 propType
检查,使用它们将被默默忽略。如果您正在使用 propTypes
,我们建议迁移到 TypeScript 或其他类型检查解决方案。
我们也从函数组件中移除了 defaultProps
,代之以 ES6 默认参数。类组件将继续支持 defaultProps
,因为没有 ES6 的替代品。
// 之前
import PropTypes from 'prop-types';
function Heading({text}) {
return <h1>{text}</h1>;
}
Heading.propTypes = {
text: PropTypes.string,
};
Heading.defaultProps = {
text: 'Hello, world!',
};
// 之后
interface Props {
text?: string;
}
function Heading({text = 'Hello, world!'}: Props) {
return <h1>{text}</h1>;
}
移除:使用 contextTypes
和 getChildContext
的旧版 Context
旧版 Context 在 2018 年 10 月 (v16.6.0) 被废弃。
旧版 Context 仅在使用 contextTypes
和 getChildContext
API 的类组件中可用,并由于易于遗漏的细微错误而被 contextType
替代。在 React 19 中,我们移除了旧版 Context,使 React 略微变小且更快。
如果您仍在类组件中使用旧版 Context,您将需要迁移到新的 contextType
API:
// 之前
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>;
}
}
// 之后
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>;
}
}
移除:字符串 ref
字符串引用在 2018 年 3 月 (v16.3.0) 被标记为不推荐使用。
在被 ref 回调函数取代之前,类组件支持字符串引用,不推荐使用字符串引用的原因在 多个缺点。在 React 19 中,我们将移除字符串引用以使 React 更加简单易懂。
如果你仍然在类组件中使用字符串引用,你需要迁移到 ref 回调函数:
// 之前
class MyComponent extends React.Component {
componentDidMount() {
this.refs.input.focus();
}
render() {
return <input ref='input' />;
}
}
// 之后
class MyComponent extends React.Component {
componentDidMount() {
this.input.focus();
}
render() {
return <input ref={input => this.input = input} />;
}
}
注意
为了帮助迁移,我们将发布一个 react-codemod 来自动替换字符串引用为 ref
回调函数。关注 这个 PR 获取更新,并尝试使用它。
移除:模块模式工厂
模块模式工厂在 2019 年 8 月 (v16.9.0) 被标记为不推荐使用。
这种模式很少被使用,且支持它会导致 React 相比必要情况下更大一些和更慢一些。在 React 19 中,我们将移除对模块模式工厂的支持,你需要迁移到常规函数:
// 之前
function FactoryComponent() {
return { render() { return <div />; } }
}
// 之后
function FactoryComponent() {
return <div />;
}
移除:React.createFactory
createFactory
在 2020 年 2 月 (v16.13.0) 被弃用。
在广泛支持 JSX 之前,使用 createFactory
是比较常见的,但现在很少使用,并且可以被 JSX 替代。在 React 19 中,我们将移除 createFactory
,你将需要迁移到 JSX:
// 之前
import { createFactory } from 'react';
const button = createFactory('button');
// 之后
const button = <button />;
移除:react-test-renderer/shallow
在 React 18 中,我们更新了 react-test-renderer/shallow
以重新导出 react-shallow-renderer。在 React 19 中,我们将移除 react-test-render/shallow
以直接安装包作为首选:
npm install react-shallow-renderer --save-dev
- import ShallowRenderer from 'react-test-renderer/shallow';
+ import ShallowRenderer from 'react-shallow-renderer';
注意
请重新考虑浅渲染
浅渲染依赖于 React 内部实现,可能会阻碍你将来的升级。我们推荐将您的测试迁移到 @testing-library/react 或 @testing-library/react-native。
移除弃用的 React DOM APIs
移除:react-dom/test-utils
我们已经将 act
从 react-dom/test-utils
移动到了 react
包:
要修复这个警告,你可以从 react
中导入 act
:
- import {act} from 'react-dom/test-utils'
+ import {act} from 'react';
所有其他 test-utils
函数已经被移除。这些工具不常见,并且使得过于容易依赖于组件和 React 的低级实现细节。在 React 19 中,调用这些函数将会报错,并且它们的导出将在未来的版本中被移除。
查看警告页面以获取替代方案。
移除:ReactDOM.render
ReactDOM.render
已于 2022 年 3 月 (v18.0.0) 被弃用。在 React 19 中,我们将移除 ReactDOM.render
,你需要迁移到使用 ReactDOM.createRoot
:
// 之前
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.hydrate
ReactDOM.hydrate
已于 2022 年 3 月 (v18.0.0) 被弃用。在 React 19 中,我们将移除 ReactDOM.hydrate
,你需要迁移到使用 ReactDOM.hydrateRoot
:
// 之前
import {hydrate} from 'react-dom';
hydrate(<App />, document.getElementById('root'));
// 之后
import {hydrateRoot} from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);
移除:unmountComponentAtNode
ReactDOM.unmountComponentAtNode
已于 2022 年 3 月 (v18.0.0) 被弃用。在 React 19 中,你需要迁移到使用 root.unmount()
。
// 之前
unmountComponentAtNode(document.getElementById('root'));
// 之后
root.unmount();
更多信息请参见 root.unmount()
,适用于 createRoot
和 hydrateRoot
。
移除:ReactDOM.findDOMNode
ReactDOM.findDOMNode
已于 2018 年 10 月 (v16.6.0) 被废弃。
我们移除 findDOMNode
是因为它是一个性能缓慢、易受重构影响的传统逃生口,并且只返回第一个子元素,并破坏了抽象层次(更多信息见这里)。你可以用 DOM refs 替代 ReactDOM.findDOMNode
:
// Before
import {findDOMNode} from 'react-dom';
function AutoselectingInput() {
useEffect(() => {
const input = findDOMNode(this);
input.select()
}, []);
return <input defaultValue="Hello" />;
}
// After
function AutoselectingInput() {
const ref = useRef(null);
useEffect(() => {
ref.current.select();
}, []);
return <input ref={ref} defaultValue="Hello" />
}
新的弃用
弃用:element.ref
React 19 支持作为一个 prop 的 ref
,所以我们弃用了 element.ref
,改用 element.props.ref
。
访问 element.ref
会显示警告:
控制台
不再支持访问 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 兼容的组件不应该注意到行为上的差异。
与所有严格模式行为一样,这些功能旨在开发阶段主动暴露组件中的错误,以便在它们被部署到生产环境之前修复它们。例如,在开发过程中,严格模式会在初始挂载时双重调用 ref 回调函数,以模拟组件被 Suspense 回退替换时发生的情况。
移除了 UMD 构建
UMD 过去被广泛使用,作为一个便捷的方式来载入 React,无需构建步骤。现在,存在现代化的替代方法作为在 HTML 文档中加载模块的脚本。从 React 19 开始,React 将不再生产 UMD 构建,以减少测试和发布过程的复杂性。
要用脚本标签加载 React 19,我们推荐使用基于 ESM 的 CDN,如 esm.sh。
<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
以迁移大部分与类型相关的破坏性更改:
npx types-react-codemod@latest preset-19 ./path-to-app
如果你有大量不稳定的对 element.props
的访问,你可以运行这个额外的 codemod:
npx types-react-codemod@latest react-element-default-any-props ./path-to-your-react-ts-files
查看 types-react-codemod
了解支持的替代列表。如果你觉得缺少一个 codemod,可以在 缺少的 React 19 codemods 列表 中跟踪它。
需要 ref
清理
此更改已包含在 react-19
codemod 预设中,作为 no-implicit-ref-callback-return
。
由于引入了 ref 清理函数,现在 TypeScript 将拒绝从 ref 回调返回任何其他内容。修复通常是停止使用隐式返回:
- <div ref={current => (instance = current)} />
+ <div ref={current => {instance = current}} />
原始代码返回了 HTMLDivElement
的实例,TypeScript 不会知道这是否应该是一个清理函数。
useRef
需要一个参数
此更改已包含在 react-19
codemod 预设中,作为 refobject-defaults
。
长期以来对 TypeScript 和 React 的工作方式的抱怨一直是 useRef
。我们已更改了类型,以便现在 useRef
需要一个参数。这极大地简化了它的类型签名。它现在将表现得更像 createContext
。
// @ts-expect-error: 预期有 1 个参数但看到了无
useRef();
// 通过
useRef(undefined);
// @ts-expect-error: 预期有 1 个参数但看到了无
createContext();
// 通过
createContext(undefined);
现在这也意味着所有的 refs 都是可变的。你将不再遇到无法修改 ref 的问题,因为你使用 null
进行了初始化:
const ref = useRef<number>(null);
// 不能给 'current' 赋值,因为它是一个只读属性
ref.current = 1;
MutableRef
现在已经被弃用,取而代之的是唯一的 RefObject
类型,useRef
将始终返回它:
interface RefObject<T> {
current: T
}
declare function useRef<T>: RefObject<T>
useRef
仍然提供了 convenience overload 用于 useRef<T>(null)
,它会自动返回 RefObject<T | null>
。为了方便迁移,因为 useRef
需要一个参数,添加了 convenience overload useRef(undefined)
,它会自动返回 RefObject<T | undefined>
。
查看 [\RFC] 使所有 refs 可变 来了解关于这一改变的之前的讨论。
对 TypeScript 类型 ReactElement
的更改
此更改包含在 react-element-default-any-props
代码修改器中。
如果元素被类型化为 ReactElement
,React 元素的 props
现在默认为 unknown
,而不是 any
。如果你为 ReactElement
传递了类型参数,那么这不会影响你:
type Example2 = ReactElement<{ id: string }>["props"];
// ^? { id: string }
但如果你依赖于默认值,现在你必须处理 unknown
:
type Example = ReactElement["props"];
// ^? 之前是 'any',现在是 'unknown'
只有当你有大量依赖不健全访问元素 props 的遗留代码时,你才需要它。元素内省仅作为逃生舱存在,你应该明确地通过显式的 any
来表示你的 props 访问是不健全的。
TypeScript 中的 JSX 命名空间
此变更包含在 react-19
代码修改器预设中,作为 scoped-jsx
长期以来的一个请求是将我们类型中的全局 JSX
命名空间改为 React.JSX
。这有助于防止全局类型的污染,避免不同利用 JSX 的 UI 库之间的冲突。
您现在需要将 JSX 命名空间的模块扩充包装在 declare module ”…”
中:
// global.d.ts
+ declare module "react" {
namespace JSX {
interface IntrinsicElements {
"my-element": {
myElementProps: string;
};
}
}
+ }
具体的模块标识符取决于您在 tsconfig.json
的 compilerOptions
中指定的 JSX 运行时:
- 对于
"jsx": "react-jsx"
应该是react/jsx-runtime
。 - 对于
"jsx": "react-jsxdev"
应该是react/jsx-dev-runtime
。 - 对于
"jsx": "react"
和"jsx": "preserve"
应该是react
。
更好的 useReducer
类型定义
多亏了 @mfp22,useReducer
现在具有改进的类型推断能力。
然而,这需要一个突破性的变更,useReducer
不再接受完整的 reducer 类型作为类型参数,而是要么不需要(依赖上下文类型),要么需要同时有状态和操作类型。
新的最佳实践是 不要 向 useReducer
传递类型参数。
- useReducer<React.Reducer<State, Action>>(reducer)
+ useReducer(reducer)
在您可以明确地输入状态和操作的边缘情况下,这可能不适用,可以通过在元组中传递 Action
来实现:
- useReducer<React.Reducer<State, Action>>(reducer)
+ useReducer<State, [Action]>(reducer)
如果您内联地定义了 reducer,我们建议您注释函数参数:
- useReducer<React.Reducer<State, Action>>((state, action) => state)
+ useReducer((state: State, action: Action) => state)
如果您将 reducer 移出 useReducer
调用,这也是您必须做的:
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_runWithPrioirty
#28271 - react-is: 从
react-is
移除废弃的方法 28224
其他值得注意的变更
- react: 批量同步,默认和连续管道 #25700
- react: 不会预渲染挂起组件的同级元素 #26380
- react: 检测由渲染阶段更新引发的无限更新循环 #26625
- react-dom: 现在 popstate 中的过渡是同步的 #26025
- react-dom: 在 SSR 期间移除布局效果警告 #26395
- react-dom: 警告且不为 src/href 设置空字符串(锚点标签除外)#28124
我们将在 React 19 稳定版发布时发布完整的更新日志。
感谢 Andrew Clark、Eli White、Jack Pope、Jan Kassens、Josh Story、Matt Carroll、Noah Lemen、Sophie Alpert 和 Sebastian Silbermann 对本文的审阅和编辑。