React 19 升级指南
2024年4月25日 作者:Ricky Hanlon
React 19 添加的改进需要一些重大更改,但我们已努力使升级尽可能顺利,并且我们预计这些更改不会影响大多数应用程序。
在这篇文章中,我们将指导您完成升级到 React 19 的步骤
如果您想帮助我们测试 React 19,请按照此升级指南中的步骤操作,并报告您遇到的任何问题。有关添加到 React 19 的新功能列表,请参阅 React 19 发行文章。
安装
安装最新版本的 React 和 React DOM
npm install --save-exact react@^19.0.0 react-dom@^19.0.0
或者,如果您使用的是 Yarn
yarn add --exact react@^19.0.0 react-dom@^19.0.0
如果您使用的是 TypeScript,则还需要更新类型。
npm install --save-exact @types/react@^19.0.0 @types/react-dom@^19.0.0
或者,如果您使用的是 Yarn
yarn add --exact @types/react@^19.0.0 @types/react-dom@^19.0.0
我们还包含了一个代码修改工具,用于最常见的替换。请参见下面的 TypeScript 更改。
代码修改工具
为了帮助进行升级,我们与 codemod.com 团队合作,发布了代码修改工具,这些代码修改工具将自动将您的代码更新到 React 19 中的许多新 API 和模式。
所有代码修改工具都可以在 react-codemod
仓库 中找到,并且 Codemod 团队也参与了代码修改工具的维护。要运行这些代码修改工具,我们建议使用 codemod
命令而不是 react-codemod
,因为它运行速度更快,处理更复杂的代码迁移,并提供更好的 TypeScript 支持。
包含代码修改的更改包括以下命令。
有关所有可用代码修改的列表,请参阅react-codemod
代码库。
重大更改
渲染错误不再重新抛出
在之前的React版本中,渲染过程中抛出的错误会被捕获并重新抛出。在开发环境中,我们还会记录到console.error
,导致错误日志重复。
在React 19中,我们改进了错误处理方式,通过不重新抛出错误来减少重复。
- 未捕获的错误:未被错误边界捕获的错误将报告给
window.reportError
。 - 已捕获的错误:被错误边界捕获的错误将报告给
console.error
。
此更改不应影响大多数应用程序,但如果您的生产错误报告依赖于重新抛出的错误,则可能需要更新您的错误处理。为了支持这一点,我们在createRoot
和hydrateRoot
中添加了新的错误处理方法。
const root = createRoot(container, {
onUncaughtError: (error, errorInfo) => {
// ... log error report
},
onCaughtError: (error, errorInfo) => {
// ... log error report
}
});
更多信息,请参阅createRoot
和hydrateRoot
的文档。
已移除弃用的React API
已移除:函数的propTypes
和defaultProps
PropTypes
已于2017年4月(v15.5.0)弃用。
在React 19中,我们从React包中移除了propType
检查,使用它们将被静默忽略。如果您正在使用propTypes
,我们建议迁移到TypeScript或其他类型检查解决方案。
我们还将函数组件中的defaultProps
替换为ES6默认参数。类组件将继续支持defaultProps
,因为没有ES6替代方案。
// Before
import PropTypes from 'prop-types';
function Heading({text}) {
return <h1>{text}</h1>;
}
Heading.propTypes = {
text: PropTypes.string,
};
Heading.defaultProps = {
text: 'Hello, world!',
};
// After
interface Props {
text?: string;
}
function Heading({text = 'Hello, world!'}: Props) {
return <h1>{text}</h1>;
}
已移除:使用contextTypes
和getChildContext
的旧版上下文
旧版上下文已于2018年10月(v16.6.0)弃用。
旧版上下文仅在类组件中使用contextTypes
和getChildContext
API可用,并被contextType
取代,原因是存在一些容易被忽略的细微错误。在React 19中,我们移除了旧版上下文,使React更小更快。
如果您仍在类组件中使用旧版上下文,则需要迁移到新的contextType
API。
// Before
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>;
}
}
// After
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
字符串ref已于2018年3月(v16.3.0)弃用。
类组件在被ref回调替换之前支持字符串ref,原因是存在多个缺点。在React 19中,我们移除了字符串ref,使React更简单易懂。
如果您仍在类组件中使用字符串ref,则需要迁移到ref回调。
// Before
class MyComponent extends React.Component {
componentDidMount() {
this.refs.input.focus();
}
render() {
return <input ref='input' />;
}
}
// After
class MyComponent extends React.Component {
componentDidMount() {
this.input.focus();
}
render() {
return <input ref={input => this.input = input} />;
}
}
已移除:模块模式工厂
模块模式工厂已于 2019年8月 (v16.9.0) 被弃用。
这种模式很少使用,并且支持它会导致 React 比必要的大一点且慢一点。在 React 19 中,我们将移除对模块模式工厂的支持,您需要迁移到常规函数。
// Before
function FactoryComponent() {
return { render() { return <div />; } }
}
// After
function FactoryComponent() {
return <div />;
}
已移除: React.createFactory
createFactory
已于 2020年2月 (v16.13.0) 被弃用。
在广泛支持 JSX 之前,使用 createFactory
很常见,但如今很少使用,可以用 JSX 代替。在 React 19 中,我们将移除 createFactory
,您需要迁移到 JSX。
// Before
import { createFactory } from 'react';
const button = createFactory('button');
// After
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 DOM API
已移除: react-dom/test-utils
我们已将 act
从 react-dom/test-utils
移动到 react
包中。
ReactDOMTestUtils.act
已被弃用,建议使用 React.act
。从 react
而不是 react-dom/test-utils
导入 act
。更多信息请参见 https://reactjs.ac.cn/warnings/react-dom-test-utils。要修复此警告,您可以从 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
。
// Before
import {render} from 'react-dom';
render(<App />, document.getElementById('root'));
// After
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
。
// Before
import {hydrate} from 'react-dom';
hydrate(<App />, document.getElementById('root'));
// After
import {hydrateRoot} from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);
已移除:ReactDOM.unmountComponentAtNode
ReactDOM.unmountComponentAtNode
已于 2022年3月 (v18.0.0) 被弃用。在 React 19 中,您需要迁移到使用 root.unmount()
。
// Before
unmountComponentAtNode(document.getElementById('root'));
// After
root.unmount();
更多信息请参见 createRoot
和 hydrateRoot
的 root.unmount()
。
已移除: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 支持 ref
作为属性,因此我们弃用 element.ref
,并用 element.props.ref
替代。
访问 element.ref
将会发出警告。
已弃用: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
将在第二次渲染期间重用第一次渲染时记忆化的结果。已经兼容 Strict Mode 的组件应该不会注意到行为上的差异。
与所有 Strict Mode 行为一样,这些功能旨在在开发过程中主动发现组件中的错误,以便在将它们发布到生产环境之前修复它们。例如,在开发过程中,StrictMode 会在初始挂载时双重调用 ref 回调函数,以模拟挂载组件被 Suspense 替代时的行为。
Suspense 的改进
在 React 19 中,当组件挂起时,React 将立即提交最近 Suspense 边界的回退内容,而无需等待整个同级树渲染完成。回退内容提交后,React 将安排对挂起同级组件的另一次渲染,以“预热”树中其余部分的延迟请求。
此更改意味着 Suspense 回退内容显示速度更快,同时仍然可以预热挂起树中的延迟请求。
移除 UMD 构建
过去,UMD 广泛用作一种方便的方法来加载 React,无需构建步骤。现在,有更现代的替代方案可以在 HTML 文档中将模块加载为脚本。从 React 19 开始,React 将不再生成 UMD 构建,以降低其测试和发布流程的复杂性。
要使用 script 标签加载 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
以了解受支持的替换列表。如果您觉得缺少 codemod,可以在 缺少的 React 19 codemod 列表 中进行跟踪。
ref
需要清理
此更改包含在 react-19
代码修改预设中,作为 no-implicit-ref-callback-return
。
由于引入了 ref 清理函数,从 ref 回调函数返回任何其他内容现在将被 TypeScript 拒绝。解决方法通常是停止使用隐式返回。
- <div ref={current => (instance = current)} />
+ <div ref={current => {instance = current}} />
原始代码返回 HTMLDivElement
的实例,TypeScript 将无法知道这是否应该是一个清理函数。
useRef
需要一个参数
此更改包含在 react-19
代码修改预设中,作为 refobject-defaults
。
长期以来,TypeScript 和 React 的工作方式一直受到 useRef
的诟病。我们更改了类型,以便 useRef
现在需要一个参数。这大大简化了它的类型签名。它现在将表现得更像 createContext
。
// @ts-expect-error: Expected 1 argument but saw none
useRef();
// Passes
useRef(undefined);
// @ts-expect-error: Expected 1 argument but saw none
createContext();
// Passes
createContext(undefined);
现在这也意味着所有 ref 都是可变的。您将不再遇到因为用 null
初始化而无法修改 ref 的问题。
const ref = useRef<number>(null);
// Cannot assign to 'current' because it is a read-only property
ref.current = 1;
MutableRef
现已弃用,取而代之的是单个 RefObject
类型,useRef
将始终返回该类型。
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
代码修改中。
如果元素类型为 ReactElement
,则 React 元素的 props
现在默认为 unknown
,而不是 any
。如果您将类型参数传递给 ReactElement
,则不会影响您。
type Example2 = ReactElement<{ id: string }>["props"];
// ^? { id: string }
但是,如果您依赖于默认值,则现在必须处理 unknown
。
type Example = ReactElement["props"];
// ^? Before, was 'any', now '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
类型声明
useReducer
现在拥有了改进的类型推断,这要感谢 @mfp22。
然而,这需要一个breaking change,useReducer
不再接受完整的reducer类型作为类型参数,而是需要不传入任何参数(依赖上下文类型推断)或者同时传入状态和action类型。
新的最佳实践是 *不要* 向 useReducer
传递类型参数。
- useReducer<React.Reducer<State, Action>>(reducer)
+ useReducer(reducer)
在一些边缘情况下,你可以通过传入 Action
元组显式地指定状态和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;
变更日志
其他breaking change
- 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:批量同步、默认和连续lane #25700
- react:不要预渲染已挂起的组件的同级组件 #26380
- react:检测由渲染阶段更新引起的无限更新循环 #26625
- react-dom:popstate中的过渡现在是同步的 #26025
- react-dom:在服务器端渲染期间移除布局效果警告 #26395
- react-dom:发出警告,并且不要为src/href设置空字符串(锚点标签除外) #28124
要查看完整的更改列表,请参阅 变更日志。
感谢 Andrew Clark,Eli White,Jack Pope,Jan Kassens,Josh Story,Matt Carroll,Noah Lemen,Sophie Alpert 和 Sebastian Silbermann 对本文的审阅和编辑。