React 18Beta 版
React 18Beta 版
简介
React 18 包括对现有功能的开箱即用的改进。它也是第一个添加对并发功能支持的 React 版本,这让您可以以 React 以前不允许的方式改善用户体验,并发功能是可选的,可以逐渐采用
安装
npm install react@beta react-dom@beta
Root API
React 17
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
React 18
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
const container = document.getElementById("root");
const root = ReactDOM.createRoot(container);
root.render(<App />);
在 React 18 中发布遗留 API 有两个原因
希望避免用户升级到 React 18 并立即看到崩溃。相反,我们向旧的根 API 添加了一个警告,建议使用新 API。
一些应用程序可能会选择运行生产实验来比较旧根和新根,其中包括开箱即用的性能改进
开箱即用的改进
自动批处理以减少渲染
setState
只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout
中都是同步的
setState
的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback)
中的callback
拿到更新后的结果
setState
的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和 setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState
,setState
的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState
多个不同的值,在更新时会对其进行合并批量更新
useLayoutEffect
其函数签名与useEffect
相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect
内部的更新计划将被同步刷新。
尽可能使用标准的 useEffect
以避免阻塞视觉更新。
function fetchSome() {
return new Promise((resolve) => setTimeout(resolve, 100));
}
const handleClick = () => {
fetchSome().then(() => {
setCount((c) => c + 1);
setFlag((f) => !f);
});
};
useLayoutEffect(() => {
console.log("Commit");
});
console.log("Render");
如果你在同一个点击事件中有两个状态更新,React 总是将它们分批处理到一个重新渲染中。,你会看到每次点击时,React 只执行一次渲染,尽管你设置了两次状态
React 18 之前,我们只在 React 事件处理程序期间批量更新。默认情况下,React 中不会对 promise、setTimeout、本机事件处理程序或任何其他事件中的更新进行批处理。
不想自动批处理
某些代码可能依赖于在状态更改后立即从 DOM 中读取某些内容。对于这些用例,可以使用ReactDOM.flushSync()
选择退出批处理
import { flushSync } from "react-dom";
const handleClick = () => {
flushSync(() => {
setCount((c) => c + 1);
});
flushSync(() => {
setFlag((f) => !f);
});
};
并发功能
React 18 将是第一个添加对并发功能的选择支持的 React 版本
React 18 将添加新功能,例如 startTransition、useDeferredValue、并发 Suspense 语义 SuspenseList、 等。为了支持这些功能,React 添加了协作多任务、基于优先级的渲染、调度和中断等概念。
Suspense
在 Legacy Suspense 和 Concurrent Suspense 中,基本的用户体验是相同的。
在 React 16.x 中添加了对 Suspense 的基本支持。但它并不完全支持 Suspense——它没有完成我们在演示中展示的所有事情,比如延迟转换(即在继续进行状态转换之前等待数据解析),或占位符限制(通过限制嵌套的、连续的占位符的出现来减少 UI 抖动)或 SuspenseList(协调组件列表或网格的外观,例如按顺序流式传输它们
用于数据的获取可以“等待”目标代码加载,并且可以直接指定一个加载页面,让它在用户等待的时候显示
<>
<Suspense
fallback={
<>
<h1>Loading 英雄联盟的英雄...</h1>
</>
}
>
<ProfileDetails resource={resource} />
<Suspense fallback={<h1>Loading 王者荣耀的英雄...</h1>}>
<ProfileTimeline resource={resource} />
</Suspense>
</Suspense>
</>
错误边界
部分 UI 的 JavaScript 错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。
错误边界是一种 React 组件,这种组件可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降级 UI,而并不会渲染那些发生崩溃的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。
注意
错误边界无法捕获以下场景中产生的错误:
- 事件处理(了解更多)
- 异步代码(例如
setTimeout
或requestAnimationFrame
回调函数) - 服务端渲染
- 它自身抛出来的错误(并非它的子组件)
在 Suspense 中,获取数据时抛出的错误和组件渲染时的报错处理方式一样——你可以在需要的层级渲染一个错误边界组件来“捕捉”层级下面的所有的报错信息
import React, { Component } from "react";
export default class ErrorComponent extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}
render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return this.props.fallback;
}
return this.props.children;
}
}
<ErrorComponent fallback={<h1>子组件出错了</h1>}>
<Suspense fallback={<h1>Loading Banner</h1>}>
<ProfileTimeline resource={resource} />
</Suspense>
</ErrorComponent>
SuspenseList
用于控制 Suspense 组件的现实顺序
加载顺序
together所有的 Suspense 一起展示
forwards按照顺序显示 Suspense
backwards反序展示 Suspense
hidden不显示
collapsed轮到自己在展示
<SuspenseList revealOrder="together">
<Suspense
fallback={
<>
<h1>Loading 当前时间</h1>
</>
}
>
<ProfileDetails resource={resource} />
</Suspense>
<Suspense fallback={<h1>Loading Banner</h1>}>
<ProfileTimeline resource={resource} />
</Suspense>
</SuspenseList>
踩坑
安装 rc 版本,SuspenseList 会显示找不到,从 TypeScript 编译器收到该错误,因为即使您已经安装了react@rc
,您仍在使用@types/react@17
. SuspenseList
确实存在于实现中,但编译器不知道
npm install react@rc react-dom@rc
要是报错如下呢:
解决方案如下:
npm install react@experimental react-dom@experimental
或者
如果您tsconfig.json
已经"types"
在该"compilerOptions"
部分中有一个数组,则添加"react/experimental"
到该"types"
数组中
startTransition
让您在状态转换期间保持 UI 响应。
作用:标记某个更新为 transition
与setTimeout
不同的是,startTransition
并不会延迟调度,而是会立即执行,startTransition
接收的函数是同步执行的,只是这个 update 被加了一个“transitions"的标记。而这个标记,React 内部处理更新的时候是会作为参考信息的。这就意味着,相比于setTimeout
, 把一个 update 交给startTransition
能够更早地被处理。而在于较快的设备上,这个过度是用户感知不到的
状态更新分成两种
Urgent updates 紧急更新,指直接交互。如点击、输入、滚动、拖拽等
Transition updates 过渡更新,如 UI 从一个视图向另一个视图的更新
使用场景
startTransition
可以用在任何你想更新的时候。
渲染慢:如果你有很多没那么着急的内容要渲染更新。
网络慢:如果你的更新需要花较多时间从服务端获取。
<Button type="primary" size="small" onClick={() => startTransition(() => {
setResource(fetchProfileData())
})}>
使用前会有 loading 展示
使用后直接过渡到数据更新后的视图,中间没有闪烁
双缓冲
useTransition
使用 startTransition 更新状态的时候,用户可能想要知道 transition 的实时情况,这个时候可以使用 React 提供的 hook api useTransition
import { useTransition } from "react";
const Button = (props) => {
const { afresh } = props;
const [isLoading, startTransition] = useTransition();
return (
<>
<button
disabled={isLoading}
onClick={() => {
startTransition(() => {
afresh();
});
}}
>
重新获取数据
</button>
</>
);
};
export default Button;
数据没有更新前,按钮为不可点击状态
useDeferredValue
让您推迟更新屏幕上不太重要的部分。
相关资源链接
React 18 发布计划
React Conf
用 createRoot 替换渲染
React 18 中 Suspense 的行为变化
全新 Suspense SSR 架构