前言
最近对公司的项目做了从React17到React18的升级,升级操作这里便不再介绍,官方文档都有。本文记录下升级到React18有哪些变动点。
TypeScript类型变动
1. FC 中不再有 children 类型
原先:
js
interface modalContentProps {
id?: string;
[key: string]: string | ReactNode;
}
现在:
js
interface modalContentProps {
id?: string;
children?: ReactNode; //需手动指定children
[key: string]: string | ReactNode;
}
2. FC 中不再包含 ref 的类型定义
原先:
js
interface Props {
ref: ForwardedRef<any>;
line: number;
}
const Single: FC<Props> = forwardRef(({ line }, ref) => {})
现在:
js
interface Props {
line: number;
}
const Single = forwardRef<ForwardedRef<any>, Props>(({ line }, ref) => {});
3. React.Key 新增 bigint 类型
原先React.key的类型为:
js
string | number
现在:
js
string | number | bigint
4. ReactNode 类型移除了对 DOM Element 的直接支持
原先:
js
// ✅ React 17 中,Element 可以赋值给 ReactNode
const element: Element = document.createElement('div');
const reactNode: React.ReactNode = element; // ✅ 编译通过
现在:
js
// ❌ React 18 中,Element 不能赋值给 ReactNode
const element: Element = document.createElement('div');
const reactNode: React.ReactNode = element; // ❌ 类型错误
行为变动
1. 并发模式与同步模式
使用新的API createRoot 开启并发模式:
js
import ReactDOM from 'react-dom/client';
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
在React 18中,依旧可以使用同步模式:
js
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
2. defaultProps属性会告警提示
原先:
js
SingleTagModal.defaultProps = {
title: '标签',
};
现在使用默认参数:
js
SingleTagModal = ({ title = '标签' }) =>{}
3. @testing-library/react-hooks弃用,合并到@testing-library/react
@testing-library/react从13版本开始支持React18的并发模式,此时@testing-library/react-hooks 的核心功能(主要是 renderHook)已内置到@testing-library/react从13中
原先:
js
import { renderHook } from '@testing-library/react-hooks'
现在:
js
import { renderHook } from '@testing-library/react'
4. 自动批处理
React 17同步模式:在React合成事件中,setState 的更新会被批处理。在JS原生事件、宏任务、微任务的情况下,setState 的更新不会被批处理。
js
//同步模式
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 会渲染两次,每次更新一个状态(没有批处理)
}, 1000);
React 18开启并发模式后,所有的更新都会自动批量处理。
js
//并发模式
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// 最终,React 将仅会重新渲染一次(批处理)
}, 1000);
示例:比较常用的ahooks的useRequest,常常在onSuccess回调中处理请求成功后的行为
useRequest部分源代码:
js
// 请求之前,将loading置为true
this.setState({
loading: true,
params,
...state,
});
// 处理请求
const res = await servicePromise;
//请求结束,将loading置为false
this.setState({
data: res,
error: undefined,
loading: false,
});
//执行onSuccess
this.options.onSuccess?.(res, params);
await后面的任务为微任务,因此同步模式与并发模式的执行结果不一样
示例:
js
import { useState } from 'react';
import { useRequest } from 'ahooks';
const getData = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
};
export default function User() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const { loading } = useRequest(getData, {
onSuccess: () => {
setCount(count + 1);
setFlag(!flag);
},
});
console.log('渲染', count, flag, loading);
return <span> {loading ? 'loading' : count}</span>;
}
同步模式下打印结果:loading置为false后,又分别渲染了两次,分别将count置为1和flag置为true
js
渲染 0 false true
渲染 0 false true
渲染 0 false false
渲染 1 false false
渲染 1 true false
并发模式下打印结果:loading置为false,与count,flag的更新是一起的
js
渲染 0 false true
渲染 0 false true
渲染 1 true false
自动批处理是一个很重要的变动点,很多升级之后带来的业务bug,都是该原因导致
5. Suspense 内的生命周期
在函数组件中的表现
同步模式:当一个树重新挂起并恢复时,不会触发任何生命周期钩子
并发模式:当一个树重新挂起时,触发useLayoutEffect的cleanup,重新恢复时,触发useLayoutEffect
在类组件中的表现
同步模式:当一个树重新挂起时,不会触发任何生命周期钩子;重新恢复时,触发componentDidUpdate
并发模式:当一个树重新挂起时,触发componentWillUnmount,重新恢复时,触发componentDidMount
对于使用react-activation实现KeepAlive功能的项目,这也是一个特别重要的变动点,react-activation本质是借助Suspense实现组件的冻结。在React 18的并发模式下,该库是有bug的。
因为componentWillUnmount被触发,导致子KeepAlive组件被误删,可通过调整生命周期的方式解决该bug。还有个问题就是useLayoutEffect触发机制调整,而antd4的动画效果是在useLayoutEffect触发,会有抖动等bug的发生,可改写rc-motion库。
新增的API与Hook
1. flushSync
- 退出批量更新
js
import React, { useState } from 'react';
import { flushSync } from 'react-dom';
const App: React.FC = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
console.log('更新')
return (
<div
onClick={() => {
flushSync(() => {
setCount1(count => count + 1);
});
// 第一次更新
flushSync(() => {
setCount2(count => count + 1);
});
// 第二次更新
}}
>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
</div>
);
};
export default App;
提示:flushSync 函数内部的多个 setState 仍然为批量更新,这样可以精准控制哪些不需要的批量更新。
- flushSync 确保了在下一行代码运行时,React 已经更新了 DOM
js
import { useRef, useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const ref = useRef(null);
const onClick = () => {
setCount((count) => count + 1);
//打印值为0,因为此时DOM还未更新
//如果使用flushSync包裹setCount,打印结果将变为1
console.log('ref', ref.current.innerHTML);
};
return (
<button onClick={onClick} ref={ref}>
{count}
</button>
);
}
export default App;
当遇到React17中没有,但在18中出现的难以解决的问题时,优先考虑使用flushSync,而非setTimeout
2. startTransition
可以让你在不阻塞 UI 的情况下更新 state
js
import { startTransition } from 'react';
// 紧急更新: 显示输入的内容
setInputValue(input);
// 将任何内部的状态更新都标记为过渡更新
startTransition(() => {
// 过渡更新: 展示结果
setSearchQuery(input);
});
3. useTransition
可以让你在不阻塞 UI 的情况下更新 state,比startTransition多个Pending状态
js
const [ isPending , startTransition ] = useTransition ()
4. useDeferredValue
让开发者延迟更新UI的某些部分。可以延迟渲染不紧急的部分,类似于防抖但没有固定的延迟时间。
js
const [value, setValue] = useState("");
const deferredValue = useDeferredValue(value);
5. useId
支持同一个组件在客户端和服务端生成相同的唯一的 ID,避免 hydration 的不兼容
js
const id = useId()
6. useSyncExternalStore
用于订阅外部数据源的变化,并确保React组件能够同步地响应这些变化。大部分情景是用来处理Redux、MobX等外部状态管理库的数据订阅。
js
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
7. useInsertionEffect
useInsertionEffect是一个专为CSS-in-JS库的开发者打造的钩子,比useLayoutEffect执行时机更早。
js
useInsertionEffect(setup, deps?)
结尾
如果还有其他比较重要的变动点,欢迎在评论区留言!!!
参考文档: