React 19 RC 升级指南

2024 年 4 月 25 日 由 Ricky Hanlon


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

注意

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 用作道具和 JSX 速度改进,这些改进需要新的变换。

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

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

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

要安装最新版本的 React 和 React DOM

npm install --save-exact react@rc react-dom@rc

或者,如果您使用的是 Yarn

yarn add --exact react@rc react-dom@rc

如果您使用的是 TypeScript,您还需要更新类型。一旦 React 19 作为稳定版本发布,您就可以像往常一样从 @types/react@types/react-dom 安装类型。在稳定版本发布之前,类型在不同的包中可用,需要在您的 package.json 中强制执行

{
"dependencies": {
"@types/react": "npm:types-react@rc",
"@types/react-dom": "npm:types-react-dom@rc"
},
"overrides": {
"@types/react": "npm:types-react@rc",
"@types/react-dom": "npm:types-react-dom@rc"
}
}

我们还包括一个代码重构,用于最常见的替换。请参阅下面的 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 用于函数

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

注意

Codemod propTypes 到 TypeScript

npx codemod@latest react/prop-types-typescript

移除:使用 contextTypesgetChildContext 的传统 Context

传统 Context 在 2018 年 10 月(v16.6.0) 被弃用。

传统 Context 仅在使用 API contextTypesgetChildContext 的类组件中可用,并被 contextType 替代,因为存在一些难以察觉的细微错误。在 React 19 中,我们移除了传统 Context,使 React 变得更小更快。

如果您仍然在类组件中使用传统 Context,则需要迁移到新的 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>;
}
}

移除:字符串 refs

字符串 refs 在 2018 年 3 月(v16.3.0) 被弃用。

类组件在被 ref 回调替代之前支持字符串 refs,这是因为 存在多个缺点。在 React 19 中,我们移除了字符串 refs,使 React 变得更简单易懂。

如果您仍然在类组件中使用字符串 refs,则需要迁移到 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} />;
}
}

注意

Codemod 字符串 refs 到 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 导入 act,而不是 react-dom/test-utils。更多信息请参考 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

移除:unmountComponentAtNode

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

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

// After
root.unmount();

有关更多信息,请参阅 root.unmount()createRoothydrateRoot

注意

unmountComponentAtNode 的 Codemod 转换为 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 作为 prop,因此我们正在弃用 element.ref,以代替 element.props.ref

访问 element.ref 将发出警告

控制台
不再支持访问 element.ref。ref 现在是一个普通的 prop。它将在将来的版本中从 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 行为一样,这些功能旨在在开发期间主动地将您的组件中的错误暴露出来,以便您可以在将它们发布到生产环境之前修复它们。例如,在开发期间,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,则可以运行此附加的代码重构。

npx types-react-codemod@latest react-element-default-any-props ./path-to-your-react-ts-files

查看 types-react-codemod 以获取支持替换的列表。如果你觉得缺少代码重构,可以在 缺少的 React 19 代码重构列表 中进行跟踪。

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 都是可变的。你将不再遇到无法修改 ref 的问题,因为你使用 null 初始化了它。

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] Make all refs mutable 以获取有关此更改的先前讨论。

ReactElement TypeScript 类型的更改

此更改包含在 react-element-default-any-props 代码重构中。

React 元素的 props 现在默认值为 unknown,而不是 any,前提是元素的类型为 ReactElement。如果您为 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 codemod 预设中,作为 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 类型

感谢 @mfp22useReducer 现在拥有改进的类型推断。

但是,这需要一个重大更改,即 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 ClarkEli WhiteJack PopeJan KassensJosh StoryMatt CarrollNoah LemenSophie AlpertSebastian Silbermann 对这篇博文的审阅和编辑。