知海

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

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

js 复制代码
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 包中移除了 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

移除:使用 contextTypes 和 getChildContext 的 Legacy Context

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

Legacy Context 仅适用于使用 contextTypes 和 getChildContext 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

createFactory 在 2020 年 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

我们已将 act 从 react-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.render 在 2022 年 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.hydrate 在 2022 年 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.unmountComponentAtNode 在 2022 年 3 月(v18.0.0) 被弃用。在 React 19 中,你需要迁移到使用 root.unmount():

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

// 之后
root.unmount();

了解更多请参阅 createRoot 和 hydrateRoot 的 root.unmount() 文档。

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

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

移除:ReactDOM.findDOMNode

ReactDOM.findDOMNode 在 2018 年 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 进行双渲染时,useMemo 和 useCallback 将在第二次渲染时复用第一次渲染的 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.json 的 compilerOptions 中指定的 JSX runtime:

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

更好的 useReducer 类型

感谢 @mfp22,useReducer 现在有了更好的类型推断。

但是,这需要一项破坏性变更: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:对 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:批量处理同步、默认和连续通道 #25700
  • react:不再预渲染挂起组件的兄弟组件 #26380
  • react:检测由渲染阶段更新引起的无限更新循环 #26625
  • react-dom:popstate 中的过渡现在是同步的 #26025
  • react-dom:移除 SSR 期间的 layout effect 警告 #26395
  • react-dom:对 src/href(锚点标签除外)为空字符串时给出警告,且不会设置该属性 #28124

完整的变更列表,请参阅更新日志。


感谢 Andrew Clark、Eli White、Jack Pope、Jan Kassens、Josh Story、Matt Carroll、Noah Lemen、Sophie Alpert 和 Sebastian Silbermann 审阅和编辑本文。

帮助我们改进文档

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