2022 年 3 月 29 日 React18 正式发布。

React18 放弃了对 IE11 的支持。

新增 createRoot API 并支持并发模式渲染

为了更好的管理 root 节点,React18 引入了一个新的 root API,新的 root API 还支持并发模式的渲染(new concurrent renderer),允许进入并发模式(concurrent mode)。

React18 从 同步不可中断的更新 变成了 异步可中断的更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// React 17
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

const root = document.getElementById('root')!;

ReactDOM.render(<App />, root);

// React 18
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = document.getElementById('root')!;

ReactDOM.createRoot(root).render(<App />);

同时,在卸载组件时,我们也需要将 unmountComponentAtNode 升级为 root.unmount :

1
2
3
4
5
// React 17
ReactDOM.unmountComponentAtNode(root);

// React 18
root.unmount();

除此之外,React18 还从 render 方法中删除了回调函数,因为当使用 Suspense 时,它通常不会有预期的结果。

在新版本中,如果需要在 render 方法中使用回调函数,我们可以在组件中通过 useEffect 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// React 17
const root = document.getElementById('root')!;
ReactDOM.render(<App />, root, () => {
console.log('渲染完成');
});

// React 18
const AppWithCallback: React.FC = () => {
useEffect(() => {
console.log('渲染完成');
}, []);
return <App />;
};
const root = document.getElementById('root')!;
ReactDOM.createRoot(root).render(<AppWithCallback />);

最后,如果项目使用了服务端渲染(SSR),需要把 ReactDOM.hydration 升级为 ReactDOM.hydrateRoot

1
2
3
4
5
6
7
8
9
// React 17
import ReactDOM from 'react-dom';
const root = document.getElementById('root');
ReactDOM.hydrate(<App />, root);

// React 18
import ReactDOM from 'react-dom/client';
const root = document.getElementById('root')!;
ReactDOM.hydrateRoot(root, <App />);

另外,还需要更新 TypeScript 类型定义,如果项目使用了 TypeScript,最值得注意的变化是,现在在定义 props 类型时,如果需要获取子组件 children ,那么需要显式的定义它,例如这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// React 17
interface MyButtonProps {
color: string;
}

const MyButton: React.FC<MyButtonProps> = ({ children }) => {
// 在 React 17 的 FC 中,默认携带了 children 属性
return <div>{children}</div>;
};

export default MyButton;

// React 18
interface MyButtonProps {
color: string;
children?: React.ReactNode;
}

const MyButton: React.FC<MyButtonProps> = ({ children }) => {
// 在 React 18 的 FC 中,不存在 children 属性,需要手动申明
return <div>{children}</div>;
};

export default MyButton;

setState 自动批量处理更新

批处理是指为了获得更好的性能,在数据层,将多个状态更新批量处理,合并成一次更新(在视图层,将多个渲染合并成一次渲染)。

在 React18 之前,只有在 react 事件处理函数中,才会自动执行批处理,其它情况会多次更新;在 React18 之后,任何情况都会自动执行批处理,多次更新始终合并为一次。

React18 通过在默认情况下执行批处理来实现了开箱即用的性能改进。

新增 flushSync 手动退出批量更新

批处理是一个破坏性改动,如果想退出批量更新,可以使用 flushSync

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { useState } from 'react';
import { flushSync } from 'react-dom';

const App: React.FC = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
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 仍然为批量更新,这样可以精准控制哪些不需要的批量更新。

删除了卸载组件后再更新组件状态时的警告

删除了以下警告:

1
Warining: Can't perfform a React state update on an unmounted component...

这个错误的初衷,原本旨在针对一些特殊场景,譬如 在 useEffect 里面设置了定时器,或者订阅了某个事件,从而在组件内部产生了副作用,而且忘记 return 一个函数清除副作用,则会发生内存泄漏之类的场景
但是在实际开发中,更多的场景是,我们在 useEffect 里面发送了一个异步请求,在异步函数还没有被 resolve 或者被 reject 的时候,我们就卸载了组件。 在这种场景中,警告同样会触发。但是,在这种情况下,组件内部并没有内存泄漏,因为这个异步函数已经被垃圾回收了,此时,警告具有误导性。

React 组件的返回值可以为 undefined

在 React17 中,如果需要返回一个空组件,React 只允许返回 null 。如果显式的返回了 undefined,控制台则会在运行时抛出一个错误。在 React18 中,不再检查因返回 undefined 而导致崩溃。既能返回 null,也能返回 undefined。

严格模式(Strict Mode)取消了第二次渲染的控制台日志

当使用严格模式时,React 会对每个组件进行两次渲染,以便观察一些意想不到的结果。在 React17 中,取消了其中一次渲染的控制台日志,以便让日志更容易阅读。如果安装了 React DevTools,第二次渲染的日志信息将显示为灰色,以柔和的方式显式在控制台。

Suspense 不再需要 fallback 来捕获

在 React18 的 Suspense 组件中,官方对空的 fallback 属性的处理方式做了改变:不再跳过缺失值或值为 null 的 fallback 的 Suspense 边界。相反,会捕获边界并且向外层查找,如果查找不到,将会把 fallback 呈现为 null。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// React 17
const App = () => {
return (
<Suspense fallback={<Loading />}> // <--- 这个边界被使用,显示 Loading 组件
<Suspense> // <--- 这个边界被跳过,没有 fallback 属性
<Page />
</Suspense>
</Suspense>
);
};

export default App;

// React 18
const App = () => {
return (
<Suspense fallback={<Loading />}> // <--- 不使用
<Suspense> // <--- 这个边界被使用,将 fallback 渲染为 null
<Page />
</Suspense>
</Suspense>
);
};

export default App;

新增 userId API

1
const id = useId();

支持同一个组件在客户端和服务端生成相同的唯一的 ID,避免 hydration 的不兼容,这解决了在 React17 及 17 以下版本中已经存在的问题。因为我们的服务器渲染时提供的 HTML 是无序的,useId 的原理就是每个 id 代表该组件在组件树中的层级结构。

新增 useSyncExternalStore API

useSyncExternalStore 能够通过强制同步更新数据让 React 组件在并发模式下安全地有效地读取外接数据源。 在并发模式下,React 一次渲染会分片执行(以 fiber 为单位),中间可能穿插优先级更高的更新。假如在高优先级的更新中改变了公共数据(比如 Redux 中的数据),那之前低优先的渲染必须要重新开始执行,否则就会出现前后状态不一致的情况。useSyncExternalStore 一般是三方状态管理库使用。React 自身的 useState 已经原生的解决了并发特性下 state 更新问题。目前 React-Redux 8.0 已经基于 useSyncExternalStore 实现。

新增 useInsertionEffect API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const useCSS = rule => {
useInsertionEffect(() => {
if (!isInserted.has(rule)) {
isInserted.add(rule);
document.head.appendChild(getStyleForRule(rule));
}
});
return rule;
};

const App: React.FC = () => {
const className = useCSS(rule);
return <div className={className} />;
};

export default App;

这个 Hook 只建议 css-in-js 库来使用。 这个 Hooks 执行时机在 DOM 生成之后,useLayoutEffect 之前,它的工作原理大致和 useLayoutEffect 相同,只是此时无法访问 DOM 节点的引用,一般用于提前注入 <style> 脚本。

新增 startTransition API

startTransition,主要为了能在大量的任务下也能保持 UI 响应。这个新的 API 可以通过将特定更新标记为“过渡”来显著改善用户交互,简单来说,就是被 startTransition 回调包裹的 setState 触发的渲染被标记为不紧急渲染,这些渲染可能被其他紧急渲染所抢占。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useState, useEffect, useTransition } from 'react';

const App: React.FC = () => {
const [list, setList] = useState<any[]>([]);
const [isPending, startTransition] = useTransition();
useEffect(() => {
// 使用了并发特性,开启并发更新
startTransition(() => {
setList(new Array(10000).fill(null));
});
}, []);
return (
<>
{list.map((_, i) => (
<div key={i}>{i}</div>
))}
</>
);
};

export default App;

由于 setListstartTransition 的回调函数中执行(使用了并发特性),所以 setList 会触发 并发更新

新增 useDeferredValue API

useDeferredValue 返回一个延迟响应的值,可以让一个 state 延迟生效,只有当前没有紧急更新时,该值才会变为最新值。useDeferredValuestartTransition 一样,都是标记了一次非紧急更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { useState, useEffect, useDeferredValue } from 'react';

const App: React.FC = () => {
const [list, setList] = useState<any[]>([]);
useEffect(() => {
setList(new Array(10000).fill(null));
}, []);
// 使用了并发特性,开启并发更新
const deferredList = useDeferredValue(list);
return (
<>
{deferredList.map((_, i) => (
<div key={i}>{i}</div>
))}
</>
);
};

export default App;