React18 新特性

Render API
// React 17
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
const root = document.querySelector('#root');
ReactDOM.render(<App />, root);
// React 18
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = document.querySelector('#root');
ReactDOM.createRoot(root).render(<App />);
React18 引入了 createRoot API(在react-dom/client下引入),使用这个 API 时内部会开启 Concurrent Mode(并发模式)。
createRoot 返回一个带有两个方法的的对象,这两个方法是:render 和 unmount。卸载组件时可以调用 unmout 方法。
// React 18
reactRoot.unmout();
// React 17
unmountComponentAtNode(root);
在 React 18,
unmountComponentAtNode已被 root.unmount() 取代,此 API 将在未来的 React 主要版本中被移除。
严格模式
严格模式可以帮助你在开发过程中(它仅在开发模式下运行)尽早地发现组件中的常见错误。开启严格模式只需要在跟组件包裹一层 <StrictMode> 组件即可。
<React.StrictMode>
<Suspense fallback={<h1>Hello~</h1>}>
<Provider>
<App />
</Provider>
</Suspense>
</React.StrictMode>
严格模式启用了以下仅在开发环境下有效的行为:
- 组件将
重新渲染一次,以查找由于非纯渲染而引起的错误。 - 组件将
重新运行 Effect 一次,以查找由于缺少 Effect 清理而引起的错误。 - 组件将被 检查是否使用了已弃用的 API。
const App = () => {
const [count, setCount] = React.useState(0);
// 严格模式下,开发环境时,会发现被打印了两次
console.log("countRender ===> ", count);
const handleClick = () => {};
useEffect(() => {
// 严格模式下,开发环境时,会发现被打印了两次
console.log('useEffect');
}, []);
return (
<div>
<button onClick={handleClick}>+1</button>
<h4>{count}</h4>
</div>
);
};
如果你安装了 React DevTools 这个浏览器插件,在 render 时打印内容,会发现多了一条暗灰色的日志,这个就是 React 内部多执行了一次渲染函数。

setState 自动批处理
在 React18 版本之前,多从 setState 时,可能会进行批处理,也可能不会,主要体现在:
- 在 React 合成事件处理函数是批处理;
- 原生事件处理函数、promise、setTimeout、任何其他事件(非React入口环境)或异步回调中是同步执行。
比如下面代码,在 React17 中,当点击按钮时,log 函数将被多次调用, 因为在 setTimeout 中 React17 不会进去批处理(promise 或者原生DOM事件也不会批处理),只有在 React 能“接管”的运行环境中才能被批处理(这通常是 React 的合成事件回调里)。
const App = () => {
const [count, setCount] = React.useState(0);
const handleClick = () => {
setTimeout(() => {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
}, 10);
};
console.log("countRender ===> ", count);
return (
<div>
<button onClick={handleClick}>+1</button>
<h4>{count}</h4>
</div>
);
};
上面代码中去掉 setTimeout 后,React17 将会进行批处理操作。而在 React18 中,非“React入口”(setTimeout、promise、原生dom事件等)也会进行批处理。
如果想跟 React17 版本行为保持一致,可以使用 flushSync API 强制渲染(通常不应该这么做,可能会影响性能)。
import React from "react";
import { flushSync } from "react-dom";
const App = () => {
const [count, setCount] = React.useState(0);
const handleClick = () => {
setTimeout(() => {
flushSync(() => {
setCount((prev) => prev + 1);
});
flushSync(() => {
setCount((prev) => prev + 1);
});
flushSync(() => {
setCount((prev) => prev + 1);
});
}, 10);
};
// 额外出发两次渲染
console.log("countRender ===> ", count);
return (
<div>
<button onClick={handleClick}>+1</button>
<h4>{count}</h4>
</div>
);
};
错误警告

这个错误原本的目的是为了防止这种问题:在 useEffect 中设置定时器或者注册原生DOM事件,组件卸载忘记取消定时器或者移除事件,这可能会引起内存泄漏。
但还有别的一些场景,比如当发起网络请求时,数据还没resolve或者reject,前端组件就被卸载销毁了,当网络请求到达时会执行 setState,也会触发这个错误警告。但其实这种情况并不会引发内存泄漏。这个警告有被误解的可能,在 React18 中该警告就被移除了。
组件的返回值
React17 中,如果你需要返回一个空组件,React只允许返回null,如果你显式的返回了undefined,控制台则会在运行时抛出一个错误。而在React18中允许返回undefined。
Suspense
在 React17 中,如果不给 Suspense 组件提供fallback属性时,React就会跳过这个Suspense组件,并向上查找Suspense组件,如果都没有找到就会以null的方式呈现fallback。
而在 React18 中,如果没有给Suspense组件提供fallback属性,React并不会向上查找,直接以null的形式呈现。
import React, { Suspense, lazy } from "react";
import ReactDOM from "react-dom";
const App = lazy(() => {
return import("./App");
});
const rootElement = document.getElementById("root");
ReactDOM.render(
{/*
React17 中第二层的 Suspense 会使用当前这一层的 fallback,
而 React18 不会向上查找 Suspense,而是用自己的 fallback,即: undefined
*/}
<Suspense fallback={<h1>Hello~</h1>}>
<Suspense>
<App />
</Suspense>
</Suspense>,
rootElement
);
Hooks
useId
可以在组件的顶层调用 useId 生成唯一 ID。React提供这个API并不是用来生成列表中唯一的key的(key 应该由你的数据生成),它主要是为无障碍属性生成唯一 ID,例如:
import { useId } from 'react';
function PasswordField() {
const passwordHintId = useId();
return (
<>
<input type="password" aria-describedby={passwordHintId} />
<p id={passwordHintId}>
</>
);
}
useInsertionEffect
useInsertionEffect 可以在布局副作用触发之前(页面DOM生成之后,useLayoutEffect被调用之前)将元素插入到 DOM 中,主要是为 CSS-in-JS 库的作者特意打造的。
需要注意的是,在 useInsertionEffect 中是访问不到 DOM 引用的,它主要的用途是往页面中动态插入 css 样式或者脚本(document 和 body 是可以访问到的)。
const App = () => {
const divElmRef = useRef<HTMLDivElement>(null);
useInsertionEffect(() => {
// { current: null }
console.log("divElmRef ===> ", divElmRef);
}, []);
return (
<div ref={divElmRef}>
</div>
);
};
三个 react effect API 的比较:
-
useEffect在组件渲染完成之后,浏览器渲染完成之后执行。它不会阻塞浏览器的渲染,是异步执行的。适合处理那些不需要同步执行的副作用,例如数据获取、订阅、或者定时器等。在一些需要异步执行的场景,使用 useEffect 可以避免阻塞渲染。 -
useLayoutEffect在所有的 DOM 变更之后、浏览器绘制之前同步执行。当你的副作用需要在 DOM 变更之前同步执行时,例如获取 DOM 元素的尺寸、位置等,如果你的副作用会影响到布局,使用useLayoutEffect可以避免闪烁或者其他不良的用户体验。 -
useInsertionEffect页面 DOM 生成之后,useLayoutEffect被调用之前。用途主要是为 CSS-in-JS 库开发者使用。
在 useEffect 和 useLayoutEffect 可以访问到 DOM,而
useInsertionEffect还不访问不到。
一般情况下,如果你的副作用不需要同步执行,最好使用 useEffect,因为它对性能的影响较小。只有在需要在布局之前同步执行副作用的情况下,才使用 useLayoutEffect。