React 19 升级指南

2024年4月25日 作者:Ricky Hanlon


React 19 添加的改进需要一些重大更改,但我们已努力使升级尽可能顺利,并且我们预计这些更改不会影响大多数应用程序。

注意

React 18.3 也已发布

为了使升级到 React 19 更容易,我们发布了 [email protected] 版本,它与 18.2 相同,但添加了对已弃用 API 和 React 19 需要其他更改的警告。

我们建议您首先升级到 React 18.3,以帮助在升级到 React 19 之前识别任何问题。

有关 18.3 中更改的列表,请参阅 发行说明

在这篇文章中,我们将指导您完成升级到 React 19 的步骤

如果您想帮助我们测试 React 19,请按照此升级指南中的步骤操作,并报告您遇到的任何问题。有关添加到 React 19 的新功能列表,请参阅 React 19 发行文章


安装

注意

现在需要新的 JSX 转换

我们在 2020 年引入了一种 新的 JSX 转换,以改进包大小并在不导入 React 的情况下使用 JSX。在 React 19 中,我们添加了更多改进,例如使用 ref 作为 prop 和 JSX 速度改进,这些改进需要新的转换。

如果未启用新的转换,您将看到此警告

控制台
您的应用程序(或其依赖项之一)正在使用过时的 JSX 转换。更新到现代 JSX 转换以获得更快的性能: https://reactjs.ac.cn/link/new-jsx-transform

我们预计大多数应用程序都不会受到影响,因为该转换已在大多数环境中启用。有关如何升级的手动说明,请参阅 公告文章

安装最新版本的 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 19 代码修改工具

使用 React 19 codemod 食谱运行本指南中列出的所有代码修改工具

npx codemod@latest react/19/migration-recipe

这将运行来自react-codemod的以下代码修改。

这并不包含TypeScript更改。请参见下面的TypeScript更改

包含代码修改的更改包括以下命令。

有关所有可用代码修改的列表,请参阅react-codemod 代码库

重大更改

渲染错误不再重新抛出

在之前的React版本中,渲染过程中抛出的错误会被捕获并重新抛出。在开发环境中,我们还会记录到console.error,导致错误日志重复。

在React 19中,我们改进了错误处理方式,通过不重新抛出错误来减少重复。

  • 未捕获的错误:未被错误边界捕获的错误将报告给window.reportError
  • 已捕获的错误:被错误边界捕获的错误将报告给console.error

此更改不应影响大多数应用程序,但如果您的生产错误报告依赖于重新抛出的错误,则可能需要更新您的错误处理。为了支持这一点,我们在createRoothydrateRoot中添加了新的错误处理方法。

const root = createRoot(container, {
onUncaughtError: (error, errorInfo) => {
// ... log error report
},
onCaughtError: (error, errorInfo) => {
// ... log error report
}
});

更多信息,请参阅createRoothydrateRoot的文档。

已移除弃用的React API

已移除:函数的propTypesdefaultProps

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>;
}

注意

使用以下代码修改将propTypes转换为TypeScript:

npx codemod@latest react/prop-types-typescript

已移除:使用contextTypesgetChildContext的旧版上下文

旧版上下文已于2018年10月(v16.6.0)弃用。

旧版上下文仅在类组件中使用contextTypesgetChildContext 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} />;
}
}

注意

使用ref回调代码修改字符串ref。

npx codemod@latest react/19/replace-string-ref

已移除:模块模式工厂

模块模式工厂已于 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 内部机制,并可能阻止您进行将来的升级。我们建议您将测试迁移到 @testing-library/react@testing-library/react-native

已移除:已弃用的 React DOM API

已移除: react-dom/test-utils

我们已将 actreact-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 中,调用这些函数将导致错误,并且它们的导出将在未来的版本中被移除。

请参阅 警告页面 以了解替代方案。

注意

使用代码修改工具将 ReactDOMTestUtils.act 修改为 React.act

npx codemod@latest react/19/replace-act-import

已移除: 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.render 迁移到 ReactDOMClient.createRoot

npx codemod@latest react/19/replace-reactdom-render

已移除: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.hydrate 迁移到 ReactDOMClient.hydrateRoot

npx codemod@latest react/19/replace-reactdom-render

已移除:ReactDOM.unmountComponentAtNode

ReactDOM.unmountComponentAtNode 已于 2022年3月 (v18.0.0) 被弃用。在 React 19 中,您需要迁移到使用 root.unmount()

// Before
unmountComponentAtNode(document.getElementById('root'));

// After
root.unmount();

更多信息请参见 createRoothydrateRootroot.unmount()

注意

使用代码迁移工具将 ReactDOM.unmountComponentAtNode 迁移到 root.unmount

npx codemod@latest react/19/replace-reactdom-render

已移除: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 将会发出警告。

控制台
不再支持访问 element.ref。ref 现在是一个普通的属性。它将在未来的版本中从 JSX 元素类型中移除。

已弃用: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 进行双重渲染时,useMemouseCallback 将在第二次渲染期间重用第一次渲染时记忆化的结果。已经兼容 Strict Mode 的组件应该不会注意到行为上的差异。

与所有 Strict Mode 行为一样,这些功能旨在在开发过程中主动发现组件中的错误,以便在将它们发布到生产环境之前修复它们。例如,在开发过程中,StrictMode 会在初始挂载时双重调用 ref 回调函数,以模拟挂载组件被 Suspense 替代时的行为。

Suspense 的改进

在 React 19 中,当组件挂起时,React 将立即提交最近 Suspense 边界的回退内容,而无需等待整个同级树渲染完成。回退内容提交后,React 将安排对挂起同级组件的另一次渲染,以“预热”树中其余部分的延迟请求。

Diagram showing a tree of three components, one parent labeled Accordion and two children labeled Panel. Both Panel components contain isActive with value false.
Diagram showing a tree of three components, one parent labeled Accordion and two children labeled Panel. Both Panel components contain isActive with value false.

以前,当组件挂起时,会先渲染挂起的同级组件,然后提交回退内容。

The same diagram as the previous, with the isActive of the first child Panel component highlighted indicating a click with the isActive value set to true. The second Panel component still contains value false.
The same diagram as the previous, with the isActive of the first child Panel component highlighted indicating a click with the isActive value set to true. The second Panel component still contains value false.

在 React 19 中,当组件挂起时,会先提交回退内容,然后渲染挂起的同级组件。

此更改意味着 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 来迁移大多数与类型相关的重大更改。

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 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.jsoncompilerOptions 中指定的 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-domsrchref 属性中的 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 ClarkEli WhiteJack PopeJan KassensJosh StoryMatt CarrollNoah LemenSophie AlpertSebastian Silbermann 对本文的审阅和编辑。