【第1793期】理解 React 的下一步:Concurrent Mode 与 Suspense
前言
【第1773期】19 个来自 2019 React Conf 的总结
今日早读文章由@林承澤分享。
正文从这开始~~
10 月刚在 Las Vegas 结束的 React Conf 2019带来许多关于 Concurrent Mode、Suspense for Data Fetching 的消息,如果你对于这些议题感到好奇,但还没有时间去吸收,那这篇文章或许值得一读。
当然除了继续往下把这篇文看完以外,如果你有充足的时间,推荐也花一点时间把官方的五篇文章看过。
以下会按照这样的编排來介绍:
什么是 Concurrent Mode?
Suspense for Data Fetching
什么是 Transition?
决定 Suspense 的揭露方式
要如何试用 Concurrent Mode?
什么是 Concurrent Mode?
说到这个就不得不提到 React 16 时,React Team 曾经把 React 整个框架重写过,整个计划「Fiber」耗时一年多才完成,其实就是为了 Concurrent Mode 所铺的路。我还特別翻到以前在 Modern Web 2017 演讲的PPT,三年也是好快就过了!(当时看到 Fiber Ready 相当感动啊)
那 Fiber 是怎么一回事呢?
为了让 Render / Reconciliation 的过程更为弹性,React Team 决定把这个一次搞定 Render 整个 Tree 的步骤切成一个个更小的步骤,让整个过程可以暂停、可以放弃也可以 Concurrent 的执行。
而 Fiber 就是 Render / Reconciliation 时的最小单位。(如果对 Reconciler 完全没概念的话推荐 Sophie Alpert 的这个介绍)
或许大部分的 React 开发者都遇到在输入框打字时,因为 State 改变造成Render 而挡住了输入框的立即更新,这个互动通常会让使用者觉得卡卡的,这个就是中断、暂停 Render 能解決的问题。
熟悉 Git 版本控制的人不妨直接用 Git 来思考 React 的运行方式,React 可以在不同的 Branch 上 Concurrent 去处理不同 State变动造成的 Render,而这些 Render 的结果可以被 Merge,也可以直接被放弃。
简而言之,React 就是要针对不同的装置能力(CPU)跟网络速度(IO)提供最优化的使用者体验。
在 React Conf 的 Keynote,Tom Occhino 提到使用者体验才是 React 的使命,我很喜欢这句原话:
A great developer experience only matters if it’s in service of delivering a great user experience. - Tom Occhino Suspense for Data Fetching
Suspense 是一个让还没准备好可以 Render 的 UI 可以显示为 Loading 状态的功能,那为什么这边要特別强调是 for Data Fetching 呢?因为 React 早先已经支持 Suspense 了,但只有包括程序代码载入的部分:
const ProfilePage = React.lazy(() => import('./ProfilePage'));
// Show a spinner while the profile is loading
<Suspense fallback={<Spinner />}>
<ProfilePage />
</Suspense>
而 Suspense for Data Fetching则堪称是这个系列的最终章,可以说是从 2016 年开始 Fiber 计划后最后一个明确的目标。
在这边需要先来了解一下官方所提出的三种获取资料的方式:
Approach 1: Fetch-on-Render
Approach 2: Fetch-Then-Render
Approach 3: Render-as-You-Fetch (using Suspense)
Fetch-on-Render
使用 componentDidMount 或是 useEffect 去获取资料就属于这种,这是理论上效率、体验最差的,Render 后才去呼叫 API,例如下面这样:
useEffect(() => {
fetchSomething().then(result => setState(result));
}, []);
而且会因为一层一层的 Render,造成获取资料时的 Waterfall。
Fetch-Then-Render
这是 Facebook 的 Relay 框架或者是说GraphQL 体系比较容易做到的事,首先必须让资料被静态的定义好。(如果不太懂 GraphQL 可以完全略过这段或是加入 GraphQL Taiwan 询问)
例如使用 GraphQL 的 Fragment,这样你才能在 Render 前就知道 Component 需要什么数据。并且让 Fragment 被 Compose 起來,就能避免获取资料時的 Waterfall。
Render-as-You-Fetch (using Suspense)
这应该会是未来推荐的做法,在 Render 之前尽早的开始获取数据,并立刻的开始 Render 下一个页面,这时资料若处于未 Ready 的状态,那就会 throw Promise 并进入 Suspense 的状态,等到 Promise Resolve 后,React 会进行 Retry(这时候资料已经 Ready 了)。
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
</Suspense>
);
}
function ProfileDetails() {
// Try to read user info, although it might not have loaded yet
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
Resource 是个未来还很有可能会改的东西,基本上可以先不用了解,只要知道他这样read,可能会 throw Promise 出去给 React接这样就夠了。
另外带來的好处 — 解決 Race Condition
以前传统的方式在 componentDidMount 或是 useEffect 去抓资数据的时候,Render 跟获取数据的 Promise 本身是脱钩的:
useEffect(() => {
fetchUser(id).then(u => setUser(u));
}, [id]);
这样的程序若在 Promise 还没 Resolve 的情況下就进行下一次的 Render,就会造成 Race Condition,因为这个 Effect 沒有被好好的 Cleanup,做干净点是要去取消 Fetch 以及它所造成的 setState 效果,但这样写清楚又很麻烦,很容易出錯。
在 Suspense for Data Fetching 的情況下,这个获取数据的 Promise 跟 Render 是挂钩一起的,就不会有这个 Effect 没完成需要取消的状况了。
什么是 Transition?
Transition 就是指切换页面的那个Transition。
为什么要特別提到这个呢?因为这在使用者体验上其实扮演举足轻重的角色。
不知道大家有沒有类似的经验,在一个已经 Render 很完整的一個页面,点了一个按钮跳页面后,那瞬间回到一个 Loading 状态,数据来了后东西才又显示出來,这中间花的时间有长有短,短得有的甚至就是一個闪烁。
以官方提供的范例来说,原本好好的 Home Page 一但切到 Profile Page,原本的画面就不见了,剩下一個大大的 Loading
在这边我们需要讨论一个状况,如果我们在跳转页面时,让原本的页面暂留一下子,来刻意地跳过中间那个有点糟的 Loading 状态,那会不会更好呢?
用 useTransition 来改善换页面的体验
React 提供了一个方式来处理这个问题,就是利用新的內建 Hook useTransition() :
import React, { useState, useTransition } from 'react';
function App() {
const [resource, setResource] = useState(initialResource);
const [startTransition, isPending] = useTransition({
timeoutMs: 3000,
});
// ...
我们简单的来看一下这个 Hook 的参数与返回值:
startTransition 是个 Function,可以用来告诉 React 哪些 State Update 可以延后生效。
isPending 是个 Boolean 值,代表 Transition 是否正在进行。这是要用来在原先的页面显示 Loading 提示,不然停在原本的页面也会让使用者以为网页失去响应。
timeoutMs 则是设定一个 Pending 的时间上限,超过了时间无论页面有多糟都是直接进行 State Update。
可以假设我们原本是这样在 onClick 里面去 setState 的:
<button
onClick={() => {
const nextUserId = getNextId(resource.userId);
setResource(fetchProfileData(nextUserId));
}}
>
可以把里面的 State Update 用 startTransition 包起來,表示这段延后生效也沒关系:
<button
onClick={() => {
startTransition(() => {
const nextUserId = getNextId(resource.userId);
setResource(fetchProfileData(nextUserId));
});
}}
>
或许大家看到这边会很疑惑,startTransition 到底在干什么?
沒关系,我刚看完也是满脸问号、一头雾水,直到我去翻了一下原始代码。
我的理解就是,包在 startTransition 里面的这段 Code 会被立刻执行,包括这个 fetchProfileData 的部分,但这个 State Update 会被用特別的 Priority放进Scheduler。
看不懂也沒关系,我们可以直接来看看它的效果,记得要回去看一下上面那張 GIF 比較一下:
这个功能帶来的结论就是下面这张图,我们要用 useTransition 来 Hold 住话面(Pending)避免走向直接切换画面所造成的 UI 倒退的狀況(Receded)。
决定 Suspense 的呈现方式
有時候,我们会有超过一个以上的 Suspense 在页面上,如果秀出超过一个Loading,有時候会蛮尷尬的,这時候可以用 SuspenseList 把它们包起來,并指定 tail 为 collapsed,这样 Loading 就只出现一个了:
<SuspenseList tail="collapsed">
<Suspense fallback={<h1>Loading...</h1>}>
</Suspense>
<Suspense fallback={<h1>Loading...</h1>}>
</Suspense>
</SuspenseList>
另一个有趣的 prop 是 revealOrder,可以用来決定呈现的順序。
来看一下 React Conf 上的 Demo,这是一个一般的版本,所有图片片參差的出現:
下面这个是 revealOrder 为 forwards 的效果,图片会从左到右,从上到下的显示:
这个是 revealOrder 为 together 的效果,所有图片一起出現:
看完这个就能知道要怎么样用这个功能来改善使用者体验了。
要如何试用 Concurrent Mode?
Concurrent Mode 目前不存在于 stable 的 release 之中,要试用的话必须安装experimental 的版本:
npm install react@experimental react-dom@experimental
除此之外,你还需要把 ReactDOM.render
改成 ReactDOM.createRoot(...).render
:
import ReactDOM from 'react-dom';
ReactDOM.createRoot(
document.getElementById('root')
).render(<App />);
如果觉得这个改动太大了,他有提供了一个介于中间的 Blocking Mode,可以用来渐进式的 Migrate 到 Concurrent Mode,虽然缺乏 useTrainsition、useDefferedValue 等等功能但比 Lagacy Mode 更接近 Concurrent Mode。(用 ReactDOM.createBlockingRoot(...).render
即可使用)
以下是功能的对照表:
出自 https://reactjs.org/docs/concurrent-mode-adoption.html
没错不要怀疑,React 就是这么的狠,把你我正在 Production 上使用的版本直接成为 Legacy Mode。
总结
Concurrent Mode 到目前为止都还在实验阶段,但可以看到 React Team 不惜花个四五年也要完成它的决心。至于要等到大部分的组件、Component 都能兼容 Concurrent Mode 也是另一个长期抗战的。
虽然不是每个开发者都需要去关注使用者不同裝置上的载入、切换的体验,但是这种事关 Reconciliation 的行为改变的原理,还是会推荐研究比較深入的 React 开发者必须了解一下。
关于本文 作者:@林承澤 原文:https://medium.com/@chentsulin/理解-react-的下一步-concurrent-mode-與-suspense-327b8a3df0fe
为你推荐