先來看一下我們想要實現的交互效果,如圖所示。
我們在前面學習了 Suspense。Suspense 的 fallback 與子組件內容的顯示是一個互斥關系。因此,當我們在請求過程中,需要顯示 Loading 時,內容就會被隱藏掉。
<Suspense fallback={<Loading />}> <Albums /></Suspense>
之前我們實現的交互效果如下。
這種交互效果其實還可以,但是許多對交互有更高要求的團隊,不會接受這樣的頁面大幅度抖動的交互。
例如在 antd 的 Table 組件中,它們選擇的方案是在列表組件上覆蓋了一個 Loading,此時的 Loading 效果與列表并非是一個互斥關系。這樣的交互體驗要比用 Loading 整體替換掉表格要好很多。
所以,在目前學習階段,我們面臨的一個困惑就是,在使用 Suspense + use 來完成功能的同時,又如何優雅的做到這種非互斥的交互效果呢?
我們想要的最佳交互效果氛圍兩個階段。
但是以目前學習到的知識點,肯定還做不到這樣的效果,因此我們要引入新的概念:useTransition。
useTransition 是 React 專門為并發模式提供的一個基礎 hook,它能夠幫助我們在不阻塞 UI 渲染的情況下更新狀態。意思就是說,我們可以借助 useTransition 將任務的優先級調得比 I/O 的響應低一些。
const [isPending, startTransition] = useTransition()
useTransition 的調用不需要參數,他的執行返回兩個參數。
isPending:是否還存在等待處理的 transition,表示被降低優先級的更新任務還沒有處理完成。
:startTransition:標記任務的優先級為 transition,該優先級低于正常任務更新。它的用法如下,我們會將更新任務在它的回調函數中執行。
function TabContainer() { const [isPending, startTransition] = useTransition(); const [tab, setTab] = useState('about'); function selectTab(nextTab) { startTransition(() => { setTab(nextTab); }); } // ……}
在優先級的排序中,被 startTransition 標記的任務比 Suspense fallback 更高一些。因此,我們可以利用這個特性,來避免 fallback 的渲染,當 startTransition 標記的任務執行完成,請求已經完成,此時 fallback 也就得不到渲染的機會了。
這里需要注意的是,標記的任務指的不是 setState ,而是對應的 UI 渲染任務,傳遞給 startTransition 的回調函數必須是同步函數。
我們可以正常這樣使用。
startTransition(() => { // ? 在調用 startTransition 中更新狀態 setPage('/about');});
但是不能在回調函數中使用異步調用。這樣會導致并發模式的任務排序出現問題。
startTransition(() => { // ? 在調用 startTransition 后更新狀態 setTimeout(() => { setPage('/about'); }, 1000);});
但是,我們可以把 startTransition 傳遞給 setTimeout。
setTimeout(() => { startTransition(() => { // ? 在調用 startTransition 中更新狀態 setPage('/about'); });}, 1000);
startTransition(async () => { await someAsyncFunction(); // ? 在調用 startTransition 后更新狀態 setPage('/about');});
await someAsyncFunction();startTransition(() => { // ? 在調用 startTransition 中更新狀態 setPage('/about');});
現在基于前面的知識點,我們來著手解決我們自己案例中的交互體驗的提升。先來回顧一下之前的代碼。
export default function Index() { const [api, setApi] = useState(getApi) function __clickToGetMessage() { setApi(getApi()) } return ( <div> <div id='tips'>點擊按鈕獲取一條新的數據</div> <button onClick={__clickToGetMessage} disabled={isPending}>獲取數據</button> <div className="content"> <Suspense fallback={<div>loading...</div>}> <Item api={api} isPending={isPending} /> </Suspense> </div> </div> )}
在這個基礎之上,我們只需要引入 useTransition 的使用即可。
export default function Index() { const [api, setApi] = useState(getApi) const [isPending, startTransition] = useTransition() function __clickToGetMessage() { startTransition(() => { setApi(getApi()) }) } ...}
setApi 所引發的任務更新被標記為 transition,他的優先級比 fallback 更高,因此此時我們需要等待 setApi 執行完成。isPending 表示是否還有待處理的 transition 任務,在這個案例中,他可以表示請求正在發生,作用與 loading 完全一致。
因此,我們可以將 isPending 傳遞給子組件,在子組件內部通過 isPending 來設計 loading UI。我們這里將組件的透明度調低。
<Item api={api} isPending={isPending} />
const Item = (props) => { const {isPending, api} = props const joke = use(api) return ( <div className='a_value' style={{opacity: isPending ? 0.5 : 1}} >{joke.value}</div> )}
最終的演示效果如下:
loading... 字樣的出現表示初始化時請求接口。整體變淺表示更新時請求接口。完整的達到了我們的訴求。
我們可以利用同樣的方式,在搜索快速輸入時做到這個交互。每一個字符的變化,在之前的嘗試中,我們都會請求一次接口。因此之前的交互如下:
我們希望如果列表已經顯示過一次,那么在搜索過程中,列表就顯示舊值,而不用切換到 fallback 的 Loading 組件。
代碼的調整與上面的案例幾乎一樣,如下:
export default function Index() { const [api, setApi] = useState(postApi) const [isPending, startTransition] = useTransition() function __inputChange() { startTransition(() => { api.cancel() setApi(postApi()) }) } ....
但是,我們注意觀察交互動畫,當我們輸入完之后,過了很長一段時間,isPending 狀態才發生變化。也就是說,在這很長的時間里,一直有 transition 任務在執行。為什么會發生這種事情呢?然后我們觀察了一下 network 面板,發現每一個請求都發生了。取消請求的代碼并沒有生效。
當然,在官方文檔中,也提到了,如果我們期望在交互過程中減少冗余請求的發生,我們可以繼續使用防抖/節流來解決問題。
本文鏈接:http://www.tebozhan.com/showinfo-26-93868-0.htmlReact Suspense 進階用法,結合 useTransition 使用
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 為什么要推薦使用現代化PHP框架?