React 19 RC 升级指南
2024 年 4 月 25 日 由 Ricky Hanlon
React 19 RC 添加的改进需要一些重大更改,但我们努力使升级尽可能平滑,并且预计这些更改不会影响大多数应用程序。
在本文中,我们将指导您完成升级到 React 19 的步骤。
如果您想帮助我们测试 React 19,请按照本升级指南中的步骤操作,并 报告遇到的任何问题。有关添加到 React 19 的新功能的列表,请参阅 React 19 发布文章。
安装
要安装最新版本的 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-codemod
仓库。
重大变更
渲染中的错误不再重新抛出
在之前的 React 版本中,渲染期间抛出的错误会被捕获并重新抛出。在开发环境中,我们还会记录到 console.error
,导致重复的错误日志。
在 React 19 中,我们已经 改进了错误处理方式,通过不再重新抛出错误来减少重复。
- 未捕获的错误:没有被错误边界捕获的错误会报告给
window.reportError
。 - 已捕获的错误:被错误边界捕获的错误会报告给
console.error
。
此更改应该不会影响大多数应用,但如果您的生产错误报告依赖于错误被重新抛出,您可能需要更新您的错误处理。为了支持这一点,我们已经在 createRoot
和 hydrateRoot
中添加了用于自定义错误处理的新方法。
const root = createRoot(container, {
onUncaughtError: (error, errorInfo) => {
// ... log error report
},
onCaughtError: (error, errorInfo) => {
// ... log error report
}
});
有关更多信息,请参阅 createRoot
和 hydrateRoot
的文档。
移除已弃用的 React API
移除: propTypes
和 defaultProps
用于函数
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>;
}
移除:使用 contextTypes
和 getChildContext
的传统 Context
传统 Context 在 2018 年 10 月(v16.6.0) 被弃用。
传统 Context 仅在使用 API contextTypes
和 getChildContext
的类组件中可用,并被 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} />;
}
}
移除:模块模式工厂
模块模式工厂在 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 DOM API
已移除:react-dom/test-utils
我们已将 act
从 react-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 中,这些函数在被调用时会报错,它们在未来版本中的导出将被移除。
请查看 警告页面 以了解替代方法。
已移除: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.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 />);
移除:unmountComponentAtNode
ReactDOM.unmountComponentAtNode
已于 2022 年 3 月(v18.0.0) 被弃用。在 React 19 中,您需要迁移到使用 root.unmount()
。
// Before
unmountComponentAtNode(document.getElementById('root'));
// After
root.unmount();
有关更多信息,请参阅 root.unmount()
的 createRoot
和 hydrateRoot
。
移除: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
将发出警告
弃用: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
将在第二次渲染期间重用第一次渲染中的已记忆结果。已经与 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
以获取支持替换的列表。如果你觉得缺少代码重构,可以在 缺少的 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.json
的 compilerOptions
中指定的 JSX 运行时。
- 对于
"jsx": "react-jsx"
,它将是react/jsx-runtime
。 - 对于
"jsx": "react-jsxdev"
,它将是react/jsx-dev-runtime
。 - 对于
"jsx": "react"
和"jsx": "preserve"
,它将是react
。
更好的 useReducer
类型
感谢 @mfp22,useReducer
现在拥有改进的类型推断。
但是,这需要一个重大更改,即 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 Clark、Eli White、Jack Pope、Jan Kassens、Josh Story、Matt Carroll、Noah Lemen、Sophie Alpert 和 Sebastian Silbermann 对这篇博文的审阅和编辑。