React Suspense: 從數據加載中汲取的經驗教訓
Suspense是React即將推出的一個功能,它有助於協調異步操作(例如數據加載),使您可以輕鬆防止UI中的狀態不一致。我將對這到底意味著什麼進行更詳細的解釋,並簡要介紹Suspense,然後介紹一個比較現實的用例,並介紹一些經驗教訓。
我介紹的功能仍處於alpha階段,絕不能用於生產環境。這篇文章是為那些想要搶先體驗即將推出的功能並了解未來發展方向的人準備的。
應用程序開發中最具挑戰性的部分之一是協調應用程序狀態和數據加載方式。狀態更改通常會在多個位置觸發新的數據加載。通常,每條數據都會有自己的加載UI(例如“旋轉器”),大致位於數據在應用程序中的位置。數據加載的異步特性意味著這些請求可以以任何順序返回。結果,您的應用程序不僅會出現許多不同的旋轉器出現和消失,更糟糕的是,您的應用程序可能會顯示不一致的數據。如果您的三個數據加載中有兩個已完成,那麼您將看到一個加載旋轉器位於第三個位置的頂部,仍然顯示舊的、現在已過時的數據。
我知道這太多了。如果您覺得其中任何內容令人費解,您可能對之前我撰寫的一篇關於Suspense的文章感興趣。該文章更詳細地介紹了Suspense是什麼以及它實現了什麼。請注意,其中一些小細節現在已經過時了,即useTransition
鉤子不再接受timeoutMs
值,而是無限期等待。
現在,讓我們快速瀏覽一下細節,然後進入一個具體的用例,其中有一些潛伏的陷阱。
幸運的是,React團隊足夠聰明,沒有將這些努力限制在僅僅加載數據上。 Suspense通過低級基元工作,您可以將其應用於幾乎任何事物。讓我們快速瀏覽一下這些基元。
首先是<suspense></suspense>
邊界,它接受一個fallback
屬性:
<suspense fallback="{<Fallback"></suspense> }>
每當此組件下的任何子組件掛起時,它都會呈現fallback
。無論有多少子組件掛起,無論出於何種原因,顯示的都是fallback
。這是React確保UI一致的一種方式——在所有內容準備就緒之前,它不會呈現任何內容。
但是,在內容最初呈現之後,用戶更改狀態並加載新數據會怎樣呢?我們當然不希望我們現有的UI消失並顯示我們的fallback
;那將是糟糕的用戶體驗。相反,我們可能希望顯示一個加載旋轉器,直到所有數據準備就緒,然後才顯示新的UI。
useTransition
鉤子實現了這一點。此鉤子返回一個函數和一個布爾值。我們調用該函數並包裝我們的狀態更改。現在事情變得有趣了。 React嘗試應用我們的狀態更改。如果任何內容掛起,React會將該布爾值設置為true
,然後等待掛起結束。完成後,它將嘗試再次應用狀態更改。也許這次它會成功,或者也許其他內容會掛起。無論如何,布爾標誌都會保持為true
,直到所有內容都準備就緒,並且只有在那時,狀態更改才會完成並反映在UI中。
最後,我們如何掛起?我們通過拋出一個promise來掛起。如果請求數據並且我們需要獲取,那麼我們獲取——並拋出一個與該獲取相關的promise。這種低級別的掛起機制意味著我們可以將其用於任何事物。用於延遲加載組件的React.lazy
實用程序已經與Suspense一起工作,我之前已經寫過關於使用Suspense在顯示UI之前等待圖像加載以防止內容移動的文章。
別擔心,我們會討論所有這些。
我們將構建一些與其他類似文章中的示例略有不同的事物。請記住,Suspense仍在alpha階段,因此您最喜歡的加載數據實用程序可能還沒有Suspense支持。但這並不意味著我們不能偽造一些東西並了解Suspense的工作原理。
讓我們構建一個無限加載列表,該列表顯示一些數據,並結合一些基於Suspense的預加載圖像。我們將顯示我們的數據,以及一個加載更多數據的按鈕。當數據呈現時,我們將預加載關聯的圖像,並在準備就緒之前掛起。
此用例基於我在我的副項目中完成的實際工作(再次,不要在生產環境中使用Suspense——但副項目是允許的)。我當時正在使用我自己的GraphQL客戶端,這篇文章的動機是我遇到的一些困難。為了簡化操作並專注於Suspense本身,而不是任何單個數據加載實用程序,我們將只偽造數據加載。
這是我們初始嘗試的沙箱。我們將使用它來逐步講解所有內容,因此現在不必急於理解所有代碼。
我們的根App
組件呈現一個像這樣的Suspense邊界:
<suspense fallback="{<Fallback"></suspense> }>
每當任何內容掛起(除非狀態更改發生在useTransition
調用中), fallback
都會呈現。為了使事情更容易理解,我使這個Fallback
組件將整個UI變成粉紅色,這樣就很難錯過;我們的目標是理解Suspense,而不是構建高質量的UI。
我們正在DataList
組件內加載當前的數據塊:
const newData = useQuery(param);
我們的useQuery
鉤子被硬編碼為返回偽造數據,包括模擬網絡請求的超時。它處理緩存結果,如果數據尚未緩存,則拋出一個promise。
我們(至少目前)將狀態保存在我們正在顯示的主數據列表中:
const [data, setData] = useState([]);
當新的數據從我們的鉤子傳入時,我們將其附加到我們的主列表中:
useEffect(() => { setData((d) => d.concat(newData)); }, [newData]);
最後,當用戶需要更多數據時,他們單擊該按鈕,該按鈕會調用此函數:
function loadMore() { startTransition(() => { setParam((x) => x 1); }); }
最後,請注意,我正在使用SuspenseImg
組件來處理我正在與每條數據一起顯示的圖像的預加載。只顯示五張隨機圖像,但我添加了一個查詢字符串以確保為我們遇到的每條新數據進行新的加載。
為了總結我們目前所處的位置,我們有一個加載當前數據的鉤子。該鉤子遵守Suspense機制,並在加載發生時拋出一個promise。每當該數據更改時,正在運行的項目總列表都會更新並附加新項目。這發生在useEffect
中。每個項目都呈現一個圖像,我們使用SuspenseImg
組件來預加載圖像,並在準備就緒之前掛起。如果您好奇某些代碼的工作原理,請查看我之前關於使用Suspense預加載圖像的文章。
如果一切正常,這將是一篇非常無聊的博客文章,別擔心,它不正常。請注意,在初始加載時,粉紅色的fallback
屏幕會顯示然後快速隱藏,但隨後會重新顯示。
當我們單擊加載更多數據的按鈕時,我們會看到內聯加載指示器(由useTransition
鉤子控制)翻轉為true
。然後我們看到它翻轉為false
,然後我們的原始粉紅色fallback
顯示。我們期望在初始加載後不再看到那個粉紅色屏幕;內聯加載指示器應該顯示直到所有內容都準備就緒。發生了什麼?
它一直隱藏在顯眼之處:
useEffect(() => { setData((d) => d.concat(newData)); }, [newData]);
useEffect
在狀態更改完成時運行,即狀態更改已完成掛起,並已應用於DOM。那部分,“已完成掛起”是這裡的關鍵。如果我們願意,我們可以在此處設置狀態,但是如果該狀態更改再次掛起,則這是一個全新的掛起。這就是為什麼我們在初始加載以及隨後數據加載完成後看到粉紅色閃爍的原因。在這兩種情況下,數據加載都已完成,然後我們在一個效果中設置狀態,這導致新數據實際呈現並再次掛起,因為圖像預加載。
那麼,我們如何解決這個問題呢?在一個層面上,解決方案很簡單:停止在效果中設置狀態。但這說起來容易做起來難。我們如何在不使用效果的情況下更新正在運行的條目列表以附加新的結果?您可能認為我們可以使用ref來跟踪事物。
不幸的是,Suspense帶來了一些關於ref的新規則,即我們不能在渲染內部設置ref。如果您想知道為什麼,請記住Suspense完全是關於React嘗試運行渲染,看到promise被拋出,然後在中途丟棄該渲染。如果我們在渲染被取消和丟棄之前更改了ref,則ref仍然具有該更改,但無效的值。渲染函數需要是純的,沒有副作用。這始終是React的一條規則,但現在它更重要了。
這是解決方案,我們將逐段講解。
首先,與其將我們的主數據列表存儲在狀態中,不如做些不同的事情:讓我們存儲我們正在查看的頁面列表。我們可以將最近的頁面存儲在ref中(儘管我們不會在渲染中寫入它),並將所有當前加載的頁面的數組存儲在狀態中。
const currentPage = useRef(0); const [pages, setPages] = useState([currentPage.current]);
為了加載更多數據,我們將相應地更新:
function loadMore() { startTransition(() => { currentPage.current = currentPage.current 1; setPages((pages) => pages.concat(currentPage.current)); }); }
然而,棘手的部分是如何將這些頁碼轉換為實際數據。我們當然不能做的是循環遍歷這些頁面並調用我們的useQuery
鉤子;鉤子不能在循環中調用。我們需要一個新的、非基於鉤子的數據API。根據我在過去的Suspense演示中看到的非常非官方的約定,我將此方法命名為read()
。它不會是一個鉤子。如果數據被緩存,它會返回請求的數據,否則會拋出一個promise。對於我們的偽造數據加載鉤子,沒有必要進行任何真正的更改;我只是簡單地複制粘貼了鉤子,然後將其重命名。但是對於實際的數據加載實用程序庫,作者可能需要做一些工作才能將這兩個選項都作為其公共API的一部分公開。在我前面提到的GraphQL客戶端中,確實同時存在useSuspenseQuery
鉤子和客戶端對像上的read()
方法。
有了這個新的read()
方法,我們代碼的最後部分就微不足道了:
const data = pages.flatMap((page) => read(page));
我們獲取每個頁面,並使用我們的read()
方法請求相應的數據。如果任何頁面未被緩存(實際上應該只有列表中的最後一個頁面),則會拋出一個promise,React會為我們掛起。當promise解析時,React會再次嘗試之前的狀態更改,這段代碼也會再次運行。
不要讓flatMap
調用迷惑你。它與map
做的事情完全相同,只是它獲取新數組中的每個結果,如果它本身是一個數組,則將其“展平”。
通過這些更改,當我們開始時,一切都會按預期工作。我們的粉紅色加載屏幕在初始加載時顯示一次,然後在後續加載中,內聯加載狀態顯示直到所有內容都準備就緒。
Suspense是對React的令人興奮的更新。它仍處於alpha階段,因此不要嘗試將其用於任何重要的事情。但是,如果您是那種喜歡搶先體驗即將推出的內容的開發人員,那麼我希望這篇文章為您提供了一些有用的背景信息和信息,這些信息在發佈時會有用。
以上是懸念:加載數據時學到的教訓的詳細內容。更多資訊請關注PHP中文網其他相關文章!