知海

如何升级到 React 19

React博客-新闻

如何升级到 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:

bash 复制代码
npx codemod@latest react/19/migration-recipe

这将运行来自 react-codemod 的以下 codemods:

这不包括 TypeScript 变更。请参阅下方的 TypeScript 变更

包含 codemod 的变更会附有相应命令。

有关所有可用 codemods 的列表,请参阅 react-codemod 仓库

破坏性变更

渲染期间的错误不再被重新抛出

在之前的 React 版本中,渲染期间抛出的错误会被捕获并重新抛出。在开发模式下,我们还会通过 console.error 记录日志,导致重复的错误日志。

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

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

此变更不应影响大多数应用,但如果你生产环境中的错误报告依赖重新抛出的错误,你可能需要更新错误处理方式。为此,我们为 createRoothydrateRoot 添加了新的自定义错误处理方法:

js 复制代码
const root = createRoot(container, {
  onUncaughtError: (error, errorInfo) => {
    // ...记录错误报告
  },
  onCaughtError: (error, errorInfo) => {
    // ...记录错误报告
  }
});

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

移除了已弃用的 React API

移除:函数组件的 propTypesdefaultProps

PropTypes2017 年 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:

bash 复制代码
npx codemod@latest react/prop-types-typescript

移除:使用 contextTypesgetChildContext 的 Legacy Context

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

Legacy Context 仅适用于使用 contextTypesgetChildContext 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 回调:

bash 复制代码
npx 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

createFactory2020 年 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

我们已将 actreact-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

bash 复制代码
npx codemod@latest react/19/replace-act-import

移除:ReactDOM.render

ReactDOM.render2022 年 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

bash 复制代码
npx codemod@latest react/19/replace-reactdom-render

移除:ReactDOM.hydrate

ReactDOM.hydrate2022 年 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

bash 复制代码
npx codemod@latest react/19/replace-reactdom-render

移除:unmountComponentAtNode

ReactDOM.unmountComponentAtNode2022 年 3 月(v18.0.0) 被弃用。在 React 19 中,你需要迁移到使用 root.unmount()

js 复制代码
// 之前
unmountComponentAtNode(document.getElementById('root'));

// 之后
root.unmount();

了解更多请参阅 createRoothydrateRootroot.unmount() 文档。

注意: 使用以下命令将 unmountComponentAtNode 迁移为 root.unmount

bash 复制代码
npx codemod@latest react/19/replace-reactdom-render

移除:ReactDOM.findDOMNode

ReactDOM.findDOMNode2018 年 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 进行双渲染时,useMemouseCallback 将在第二次渲染时复用第一次渲染的 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 来迁移大多数与类型相关的破坏性变更:

bash 复制代码
npx types-react-codemod@latest preset-19 ./path-to-app

如果你有大量对 element.props 的不安全访问,可以运行这个额外的 codemod:

bash 复制代码
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 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.jsoncompilerOptions 中指定的 JSX runtime:

  • 对于 "jsx": "react-jsx",应为 react/jsx-runtime
  • 对于 "jsx": "react-jsxdev",应为 react/jsx-dev-runtime
  • 对于 "jsx": "react""jsx": "preserve",应为 react

更好的 useReducer 类型

感谢 @mfp22useReducer 现在有了更好的类型推断。

但是,这需要一项破坏性变更: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:对 srchref 中的 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 ClarkEli WhiteJack PopeJan KassensJosh StoryMatt CarrollNoah LemenSophie AlpertSebastian Silbermann 审阅和编辑本文。

帮助我们改进文档

发现翻译问题或内容错误?请告诉我们。