译:React 19 Beta Upgrade Guide

原文:https://react.dev/blog/2024/04/25/react-19-upgrade-guide
作者:Ricky Hanlon
译者:ChatGPT 4 Turbo

注意

此 beta 版本是为了让库准备迁移到 React 19。应用开发者应该升级到 18.3.0,并等待 React 19 的稳定版本,因为我们正在与库合作并根据反馈进行更改。

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

为了帮助简化升级过程,今天我们还发布了 React 18.3。

注意

也已发布 React 18.3

为了使升级到 React 19 更加容易,我们发布了一个 react@18.3 版本,它与 18.2 相同,但增加了对已弃用 API 和其他为 React 19 准备的更改的警告。

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

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

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

如果你想帮助我们测试 React 19,请按照此升级指南中的步骤操作,并报告你遇到的任何问题。查看 React 19 beta 新增功能的列表,请参阅 React 19 发布文章


安装

注意

现在需要新的 JSX 转换

我们在 2020 年引入了新的 JSX 转换,以改善包大小并无需导入 React 就能使用 JSX。在 React 19 中,我们增加了像将 ref 作为 prop 和 JSX 速度提升这样的额外改进,这需要新的转换。

如果新的变换没有启用,你会看到这个警告:

我们预计大多数应用不会受到影响,因为在大多数环境中这个变换已经被启用。有关如何升级的手动说明,请参见公告帖子

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

npm install react@beta react-dom@beta

如果你在使用 TypeScript,你还需要更新类型定义。一旦 React 19 作为稳定版发布,你可以像往常一样从 @types/react@types/react-dom 安装类型定义。在测试期间,类型定义在不同的包中,需要在你的 package.json 中强制使用:

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

我们还提供了一个 codemod,用于最常见的替换。下面见 TypeScript 变动

破坏性变动

渲染中的错误不会被重新抛出

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

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

  • 未捕获的错误:没有被错误边界捕获的错误会报告给 window.reportError
  • 捕获的错误:被错误边界捕获的错误会报告给 console.error

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

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

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

移除了废弃的 React API

移除:函数的 propTypesdefaultProps

PropTypes2017 年 4 月 (v15.5.0) 被废弃。

在 React 19 中,我们从 React 包中移除了 propType 检查,使用它们将被默默忽略。如果您正在使用 propTypes,我们建议迁移到 TypeScript 或其他类型检查解决方案。

我们也从函数组件中移除了 defaultProps,代之以 ES6 默认参数。类组件将继续支持 defaultProps,因为没有 ES6 的替代品。

// 之前
import PropTypes from 'prop-types';

function Heading({text}) {
  return <h1>{text}</h1>;
}
Heading.propTypes = {
  text: PropTypes.string,
};
Heading.defaultProps = {
  text: 'Hello, world!',
};
// 之后
interface Props {
  text?: string;
}
function Heading({text = 'Hello, world!'}: Props) {
  return <h1>{text}</h1>;
}

移除:使用 contextTypesgetChildContext 的旧版 Context

旧版 Context 在 2018 年 10 月 (v16.6.0) 被废弃。

旧版 Context 仅在使用 contextTypesgetChildContext API 的类组件中可用,并由于易于遗漏的细微错误而被 contextType 替代。在 React 19 中,我们移除了旧版 Context,使 React 略微变小且更快。

如果您仍在类组件中使用旧版 Context,您将需要迁移到新的 contextType API:

// 之前
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>;
  }
}
// 之后
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

字符串引用在 2018 年 3 月 (v16.3.0) 被标记为不推荐使用。

在被 ref 回调函数取代之前,类组件支持字符串引用,不推荐使用字符串引用的原因在 多个缺点。在 React 19 中,我们将移除字符串引用以使 React 更加简单易懂。

如果你仍然在类组件中使用字符串引用,你需要迁移到 ref 回调函数:

// 之前
class MyComponent extends React.Component {
  componentDidMount() {
    this.refs.input.focus();
  }

  render() {
    return <input ref='input' />;
  }
}
// 之后
class MyComponent extends React.Component {
  componentDidMount() {
    this.input.focus();
  }

  render() {
    return <input ref={input => this.input = input} />;
  }
}

注意

为了帮助迁移,我们将发布一个 react-codemod 来自动替换字符串引用为 ref 回调函数。关注 这个 PR 获取更新,并尝试使用它。

移除:模块模式工厂

模块模式工厂在 2019 年 8 月 (v16.9.0) 被标记为不推荐使用。

这种模式很少被使用,且支持它会导致 React 相比必要情况下更大一些和更慢一些。在 React 19 中,我们将移除对模块模式工厂的支持,你需要迁移到常规函数:

// 之前
function FactoryComponent() {
  return { render() { return <div />; } }
}
// 之后
function FactoryComponent() {
  return <div />;
}

移除:React.createFactory

createFactory2020 年 2 月 (v16.13.0) 被弃用。

在广泛支持 JSX 之前,使用 createFactory 是比较常见的,但现在很少使用,并且可以被 JSX 替代。在 React 19 中,我们将移除 createFactory,你将需要迁移到 JSX:

// 之前
import { createFactory } from 'react';

const button = createFactory('button');
// 之后
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 APIs

移除:react-dom/test-utils

我们已经将 actreact-dom/test-utils 移动到了 react 包:

要修复这个警告,你可以从 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

// 之前
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.hydrate

ReactDOM.hydrate 已于 2022 年 3 月 (v18.0.0) 被弃用。在 React 19 中,我们将移除 ReactDOM.hydrate,你需要迁移到使用 ReactDOM.hydrateRoot

// 之前
import {hydrate} from 'react-dom';
hydrate(<App />, document.getElementById('root'));

// 之后
import {hydrateRoot} from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);

移除:unmountComponentAtNode

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

// 之前
unmountComponentAtNode(document.getElementById('root'));

// 之后
root.unmount();

更多信息请参见 root.unmount(),适用于 createRoothydrateRoot

移除: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 支持作为一个 prop 的 ref,所以我们弃用了 element.ref,改用 element.props.ref

访问 element.ref 会显示警告:

控制台

不再支持访问 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 兼容的组件不应该注意到行为上的差异。

与所有严格模式行为一样,这些功能旨在开发阶段主动暴露组件中的错误,以便在它们被部署到生产环境之前修复它们。例如,在开发过程中,严格模式会在初始挂载时双重调用 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 的访问,你可以运行这个额外的 codemod:

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

查看 types-react-codemod 了解支持的替代列表。如果你觉得缺少一个 codemod,可以在 缺少的 React 19 codemods 列表 中跟踪它。

需要 ref 清理

此更改已包含在 react-19 codemod 预设中,作为 no-implicit-ref-callback-return

由于引入了 ref 清理函数,现在 TypeScript 将拒绝从 ref 回调返回任何其他内容。修复通常是停止使用隐式返回:

- <div ref={current => (instance = current)} />
+ <div ref={current => {instance = current}} />

原始代码返回了 HTMLDivElement 的实例,TypeScript 不会知道这是否应该是一个清理函数。

useRef 需要一个参数

此更改已包含在 react-19 codemod 预设中,作为 refobject-defaults

长期以来对 TypeScript 和 React 的工作方式的抱怨一直是 useRef。我们已更改了类型,以便现在 useRef 需要一个参数。这极大地简化了它的类型签名。它现在将表现得更像 createContext

// @ts-expect-error: 预期有 1 个参数但看到了无
useRef();
// 通过
useRef(undefined);
// @ts-expect-error: 预期有 1 个参数但看到了无
createContext();
// 通过
createContext(undefined);

现在这也意味着所有的 refs 都是可变的。你将不再遇到无法修改 ref 的问题,因为你使用 null 进行了初始化:

const ref = useRef<number>(null);

// 不能给 'current' 赋值,因为它是一个只读属性
ref.current = 1;

MutableRef 现在已经被弃用,取而代之的是唯一的 RefObject 类型,useRef 将始终返回它:

interface RefObject<T> {
  current: T
}

declare function useRef<T>: RefObject<T>

useRef 仍然提供了 convenience overload 用于 useRef<T>(null),它会自动返回 RefObject<T | null>。为了方便迁移,因为 useRef 需要一个参数,添加了 convenience overload useRef(undefined),它会自动返回 RefObject<T | undefined>

查看 [\RFC] 使所有 refs 可变 来了解关于这一改变的之前的讨论。

对 TypeScript 类型 ReactElement 的更改

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

如果元素被类型化为 ReactElement,React 元素的 props 现在默认为 unknown,而不是 any。如果你为 ReactElement 传递了类型参数,那么这不会影响你:

type Example2 = ReactElement<{ id: string }>["props"];
//   ^? { id: string }

但如果你依赖于默认值,现在你必须处理 unknown

type Example = ReactElement["props"];
//   ^? 之前是 'any',现在是 '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 类型定义

多亏了 @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 对本文的审阅和编辑。