這篇文章中,我將仔細研究Recoil中的異步查詢。我將向你展示該庫的一些功能以及它如何與React無縫結合。
我們開始吧。
Recoil是什么
Recoil是一個狀態管理庫,將狀態映射到React組件。當狀態是異步的時候,selectors將會在數據流中表現的像一個純函數。編程接口依然是熟悉的樣子,但是它將會返回一個Promise而不是一個值。
消費組件從感覺像是同步查詢的seloctor函數中獲取它們所需要的值。這允許我們使用一些強大的技術,比如React Suspense加載器,緩存,和預取請求。
設置
你可以在Github上找到實例代碼。我建議你clone並運行下,以便更好的了解扎個庫。我使用 json-server
來托管鯨魚物種的數據。這個應用加載一些可選的鯨魚物種然后你可以選擇其中一個物種來查看其更多詳細信息。AJAX請求提供app中的狀態,Recoil映射異步的狀態到React組件中。
我將引用大量的實例代碼。要啟動這個項目,在命令行中依次運行: npm run json-server
npm start
. API響應中有個3秒的延遲來說明Recoil中的功能。你可以在package.json中查看延遲,使用 30001 端口
托管數據。並設置一個代理 http://localhost:3001
。這使得CRA應用知道去哪里獲取異步的數據。代碼將引用 /whales/blue_whale
作為獲取數據的路由,沒有主機和端口。
React Suspense
<Suspense />
組件聲明性的等待數據加載並定義了一個loading狀態。當狀態為異步時,Recoil鈎入React組件。如果一個異步請求沒有被包到Suspense中將無法編譯。不過有一個解決辦法就是 使用useRecoilValueLoadable
,但是這種情況下需要更多代碼來追蹤狀態:(博主之前就是使用這種方法,要寫很多代碼,實例如下)
function UserInfo({userID}) {
const userNameLoadable = useRecoilValueLoadable(userNameQuery(userID));
switch (userNameLoadable.state) {
case 'hasValue':
return <div>{userNameLoadable.contents}</div>;
case 'loading':
return <div>Loading...</div>;
case 'hasError':
throw userNameLoadable.contents;
}
}
Recoil可以依賴Suspense組件,寫法如下:
<RecoilRoot>
<Suspense fallback={<div>Loading whale types...</div>}>
<CurrentWhaleTypes />
<Suspense fallback={
<div>Loading <CurrentWhaleIdValue /> info...</div>
}> {/* nested */}
<CurrentWhalePick />
</Suspense>
</Suspense>
</RecoilRoot>
fallback
聲明了加載組件,也可以是其他使用Recoil狀態的組件。加載器可以被嵌套,所以一次只顯示一個加載器,這個聲明式定義了如何以及何時在app中加載數據。
當你選擇一個鯨魚,<CurrentWhalePick />
開始加載,CurrentWhaleIdValue
組件在 fallback
加載器中。
function CurrentWhaleIdValue() {
const whaleId = useRecoilValue(currentWhaleIdState)
return (
<span>{whaleId.replace('_', ' ')}</span>
)
}
currentWhaleIdState
是一個Recoil atom,它是查詢參數的真是來源。選擇一個鯨魚設置鯨魚id,這個值將會顯示在加載器中。我鼓勵你查看currentWhaleIdState
是如何定義的,因為這是Recoil去緩存請求的依賴。
CurrentWhalePick
組件通過一個查詢selector獲取異步狀態。Recoil有useRecoilValue
鈎子:觸發最初的請求,當組件沒有包裹在<Suspense />
中時會拋出一個錯誤。
很棒的一點是,useRecoilValue
鈎子可以被用來調用同步數據的selectors。一個關鍵的不同就是同步調用不用被包裹在Suspense組件中。
function CurrentWhalePick() {
const whale = useRecoilValue(currentWhaleQuery) // fire AJAX request
return (
<>
{whale === undefined
? <p>Please choose a whale.</p> // zero-config
: <>
<h3>{whale.name}</h3>
<p>Life span: {whale.maxLifeSpan} yrs</p>
<p>Diet: {whale.diet} ({whale.favoriteFood})</p>
<p>Length: {whale.maxLengthInFt} ft</p>
<p>{whale.description}</p>
<img alt={whale.id} src={whale.imgSrc} />
</>
}
</>
)
}
當鯨魚是undefined
時,app是零配置狀態。當一切加載完畢時,最好向用戶指示下一步要做什么(提示用戶選擇一個鯨魚物種),而Recoil狀態使這一切變得很easy。
緩存
Recoil selector函數是冪等的,也就意味着一個給定的輸入肯定會返回相同的值。在異步請求中,這個變得相當重要因為響應可以被緩存。
當selector函數獲取到參數依賴,Recoil自動緩存響應。下次組件用相同的輸入請求時,selector返回一個fullfilled狀態的帶有緩存數據的Promise。
這就是Recoil如何觸發一個請求並自動緩存響應。
const currentWhaleQuery = selector({
key: 'CurrentWhaleQuery',
get: ({get}) =>
get(whaleInfoQuery(get(currentWhaleIdState)))
})
查詢參數currentWhaleIdState
是一個atom:返回一個基礎字符串類型。Recoil做一個簡單的相等檢查當它被設置到緩存key查找表中時。如何參數依賴是被一個復雜類型包裹,然后相等操作符無法識別緩存key,這會破壞緩存。這是因為js中的相等檢查時,對象每次的指針都是不同的。Recoil中的狀態突變都是不可變的,對復雜類型的更改會設置一個新的實例。
一個建議是設置參數為簡單類型來啟用緩存--避免復雜類型除非你計划每次都破壞緩存。
如果你正在跟隨正在運行的app,例第一點擊Blue Whale需要3秒才能加載。點擊其他鯨魚種類也是需要等3秒,但是如果再次點擊之前點過的,它會直接加載。這就是說Recoil緩存機制在起作用。
預取請求
Recoil使得當點擊時間觸發時立即觸發請求成為可能。在React組件生命周期中不會執行請求直到傳入了一個新的鯨魚id參數觸發組件渲染。這有一個小小的延遲在點擊事件和ajax請求之間。鑒於網絡上的異步請求速度較慢,盡快開始執行請求是有好處的。
為了使得查詢能夠預取,使用 selectorFamily
而不是 selector
:
const whaleInfoQuery = selectorFamily({
key: 'WhaleInfoQuery', // diff `key` per selector
get: whaleId => async () => {
if (whaleId === '') return undefined
const response = await fetch('/whales/' + whaleId)
return await response.json()
}
})
selector函數使有一個來自於前一個selector中get
的whaleid
參數。這個參數和 currentWhaleIdState
atom有相同的值。key
在不同的selector中時不同的,但是whalse id參數是相同的。
whaleInfoQuery
selector可以在你選中一個鯨魚后立馬觸發請求因為它的函數參數中有whaleId
參數。
接下來:事件處理程序從e事件參數中獲取鯨魚id:
function CurrentWhaleTypes() {
const whaleTypes = useRecoilValue(currentWhaleTypesQuery)
return (
<ul>
{whaleTypes.map(whale =>
<li key={whale.id}>
<a
href={"#" + whale.id}
onClick={(e) => {
e.preventDefault()
changeWhale(whale.id)
}}
>
{whale.name}
</a>
</li>
)}
</ul>
)
}
這個組件渲染鯨魚列表。它循環響應並且設置一個事件處理函數在html元素上。preventDefault
編程式地阻止瀏覽器把它當成一個實際的a標簽跳轉連接。
changeWhale
函數會立即從選中的元素捕獲鯨魚id並且變為最新的參數。一個useSetRecoilState
鈎子也會改變鯨魚id,但會將請求延遲到下次渲染。
因為為更喜歡用預取請求,changeWhale
函數如下:
const changeWhale = useRecoilCallback(
({snapshot, set}) => whaleId => {
snapshot.getLoadable(whaleInfoQuery(whaleId)) // pre-fetch
set(currentWhaleIdState, whaleId)
}
)
獲取snapshot 並使用 set 立即改變狀態,並使用查詢和參數調用 getLoadable以觸發請求。set和getLoadable之間的順序無關緊要,因為whaleInfoQuery已經使用了必要的參數調用了查詢。當組件重新渲染時,set保證了鯨魚id的變化。
為了證明預取是有效果的,在調用fetch時在whaleInfoQuery中設置一個斷點。檢查調用堆棧並在堆棧的地步查找CurentWhaleTypes———這將執行onClick事件。如果恰好是CurrentWhalePick,則請求會在重新渲染時觸發,而不是在點擊事件中觸發。
可以通過 useSetRecoilState
和 changeWhale
在預取和重新渲染之間交換查詢。github上的已經注釋掉可交換代碼。我建議你把玩以下:交換重新渲染並查看調用堆棧。改回預取調用調用來自點擊事件的查詢。
總結
這就是demo的最終效果
總之,Recoil有一些優秀的異步狀態特性:
- 通過
<Suspense />
包裝器和后備組件與 React Suspense 無縫集成以在加載期間顯示 - 自動緩存數據,假設selector函數使冪等的
- 發生點擊事件時立即觸發異步查詢,以提升性能