Reactv19 已经发布 beta 版本,想要快速体验如何升级到 v19 版本尝鲜的朋友们可以查阅进行了解
前言
React 已于近日发布了 v19
的 beta 版本,同时为了帮助后续的 v19
升级,也同时发布了 v18.3.0
的正式版, 与 v18.2
版本完全相同,但添加了弃用 API 的警告和其他为 React 19 所需的更改
安装
使用新版 JSX Transform
为了改善打包体积和可以在 JSX 文件中无需手动引入 React
,在 2020 年 React 引入了新的 JSX Transform。如果在 React 19 中没有使用这个新的 JSX Transform 会有一个报错提示
如果已经使用了新版 JSX Transform 则可以忽略此步骤
安装最新版本的 React 和 ReactDom
npm install react@beta react-dom@beta
如果使用 TypeScript,则还需要更新相关类型包。等到 React 19 发布 release 版本后可以就像往常一样从@types/react
和@types/react-dom
安装类型包。在当前 beta 版本中需要在package.json
为类型包配置overrides
锁定版本以确保不同包中的类型是可用的
{
"dependencies": {
"@types/react": "npm:types-react@beta",
"@types/react-dom": "npm:types-react-dom@beta"
},
"overrides": {
"@types/react": "npm:types-react@beta",
"@types/react-dom": "npm:types-react-dom@beta"
}
}
Breaking changes
render 过程中的错误不再二次抛出
在之前的 React 版本中,渲染过程中抛出的错误会被捕获并重新抛出。在 DEV 模式下,我们还会记录到 console.error,导致出现重复的错误日志。
在 React 19 中,改进了错误处理方式,通过不重新抛出来减少重复信息:
- 未捕获的错误:未被错误边界捕获的错误将调用给
window.reportError
- 已捕获的错误:被错误边界捕获的错误将报告将调用给
console.error
这个改变不应该影响大多数应用,但如果生产错误报告依赖于错误被重新抛出,则可能需要更新错误处理。为了支持这一点,React 19 添加了新的createRoot
和hydrateRoot
用于自定义错误处理:
const root = createRoot(container, {
onUncaughtError: (error, errorInfo) => {
// ... log error report
},
onCaughtError: (error, errorInfo) => {
// ... log error report
}
});
废弃 React API 移除
移除propTypes
和函数组件的defaultProps
propTypes
是用于运行时校验组件 props 的属性,在 Reactv15.5.0
已经被标记为废弃,在 v19
这个正式删除
另外函数组件的defaultProps
也已经移除(使用 ES6 默认参数替代),由于 class 组件没有相应的 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
的 Legacy Context
Legacy Context 在2018.10(v16.6.0)已被弃用
Legacy Context 仅适用于使用contextTypes
和getChildContext
API 的类组件,并由于易于忽略的微妙错误而被contextType
替换。在 React 19 中,将删除 Legacy Context 以使 React 更小更快。仍在类组件中使用 Legacy 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 中,将删除字符串引用以使 React 更简单易懂
// Before
class MyComponent extends React.Component {
componentDidMount() {
this.refs.input.focus();
}
render() {
return <input ref='input' />;
}
}
如果仍在使用类组件中的字符串引用,则需要迁移到 refs 回调的形式:
// After
class MyComponent extends React.Component {
componentDidMount() {
this.input.focus();
}
render() {
return <input ref={input => this.input = input} />;
}
}
移除模块模式工厂
模块模式工厂在2019.8(v16.9.0)被弃用。
// Before
function FactoryComponent() {
return { render() { return <div />; } }
}
这种用法其实很少使用,支持它会使 React 比必要的更大和更慢。在 React 19 中,将删除对模块模式工厂的支持,需要迁移到常规函数:
// After
function FactoryComponent() {
return <div />;
}
移除React.createFactory
createFactory 在2020.2(v16.13.0)已被弃用。
// Before
import { createFactory } from 'react';
const button = createFactory('button');
在 JSX 得到广泛支持之前使用 createFactory 很常见,但是现在已经可以用 JSX 替换。在 React 19 中,将删除 createFactory,需要迁移到 JSX
// 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';
废弃 ReactDOM API 移除
移除react-dom/test-utils
移除ReactDOM.render
ReactDOM.render
在2022 年 3 月(v18.0.0)已被弃用。
// Before
import {render} from 'react-dom';
render(<App />, document.getElementById('root'));
在 React 19 中,将删除 ReactDOM.render,需要迁移到使用ReactDOM.createRoot
:
// 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 中需要迁移到使用hydrateRoot
和createRoot
对应的root.unmount()
// Before
unmountComponentAtNode(document.getElementById('root'));
// After
root.unmount();
移除ReactDOM.findDOMNode
ReactDOM.findDOMNode 在2018 年 10 月(v16.6.0)已被弃用
// Before
import {findDOMNode} from 'react-dom';
function AutoselectingInput() {
useEffect(() => {
const input = findDOMNode(this);
input.select()
}, []);
return <input defaultValue="Hello" />;
}
可以使用 DOM 引用替换 ReactDOM.findDOMNode
// 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
会出现警告
function MyInput({placeholder, ref}) {
return <input placeholder={placeholder} ref={ref} />
}
//...
<MyInput ref={ref} />
新的函数组件将不再需要forwardRef
,在未来的版本中,React 将弃用并删除forwardRef
但是传递给类的 refs
不会作为 props 传递,因为refs
引用的是组件实例
废弃react-test-renderer
弃用react-test-renderer
。react-test-renderer
实现了自己的渲染器环境与用户使用的环境不匹配并依赖于 React 内部的实现细节
在 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 内部的更改,可能会影响那些忽略 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 类型。同时提供了一个types-react-codemod
工具可以帮助迁移已有的类型
npx types-react-codemod@latest preset-19 ./path-to-app
ref
返回内容必须是清理函数
由于引入了ref
清理函数,从ref
回调返回任何其他内容现在将被 TypeScript 报错。修复方法通常是停止使用隐式返回:
- <div ref={current => (instance = current)} />
+ <div ref={current => {instance = current}} />
原始代码返回HTMLDivElement
的实例,TypeScript 无法确定是否清理函数。
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);
现在也意味着所有的引用都是可变的。不再会遇到以下的问题,传递 number
类型但是使用 null
初始化
// before
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(null),它自动返回 RefObject<T | null>。为了简化由于 useRef 所需参数的迁移,添加了一个方便的重载 useRef(undefined),它自动返回 RefObject<T | undefined>。
ReactElement
类型变化
如果元素被标记为ReactElement
,则ReactElement
的props
现在默认为unknown
而不是any
。如果向ReactElement
传递类型参数则不会受到影响
type Example2 = ReactElement<{ id: string }>["props"];
// ^? { id: string }
但是如果依赖默认设置,则需要处理unknown
:
type Example = ReactElement["props"];
// ^? Before, was 'any', now 'unknown'
TypeScript 中的 JSX namespace 变化
类型中删除全局JSX
命名空间转而使用React.JSX
。防止全局类型的污染和不同 UI 库之间利用 JSX 产生冲突
现在,需要在declare module
中的JSX
命名空间的模块进行修改
// 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
类型
useReducer
类型推断得到了改善。然而这需要一个破坏性的变化,其中useReducer
不再接受完整的reducer
类型作为类型参数,而是需要接收State
和Action
的类型
新的最佳实践是不要向 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;