ReactQuery系列React?Query?實(shí)踐示例詳解
引言
當(dāng)2018年GraphQL特別是Apolllo Client開(kāi)始流行之后,很多人開(kāi)始認(rèn)為它將替代Redux,關(guān)于Redux是否已經(jīng)落伍的問(wèn)題經(jīng)常被問(wèn)到。
我很清晰地記得我當(dāng)時(shí)對(duì)這些觀點(diǎn)的不理解。為什么一些數(shù)據(jù)請(qǐng)求的庫(kù)會(huì)替代全局狀態(tài)管理庫(kù)呢?這兩者有什么關(guān)聯(lián)呢?
曾經(jīng)我認(rèn)為像Apollo這樣的Graphql客戶端只能用來(lái)請(qǐng)求數(shù)據(jù),就像axios一樣,你仍然需要一些方式來(lái)讓請(qǐng)求的數(shù)據(jù)可以被應(yīng)用程序訪問(wèn)到。
我發(fā)現(xiàn)我大錯(cuò)特錯(cuò)。
客戶端狀態(tài) vs 服務(wù)端狀態(tài)
Apollo提供的不僅僅是描述所需數(shù)據(jù)同時(shí)獲取數(shù)據(jù)的能力,它同時(shí)提供了針對(duì)這些服務(wù)端數(shù)據(jù)的緩存能力。這意味著你可以在多個(gè)組件中使用相同的useQuery
hook,它只會(huì)觸發(fā)一次數(shù)據(jù)請(qǐng)求并且按照請(qǐng)求的先后順序返回緩存中的數(shù)據(jù)。
這看起來(lái)跟我們(包括很多除了我們以外的團(tuán)隊(duì))在一些場(chǎng)景使用redux
的目的很相似:從服務(wù)器獲取數(shù)據(jù),然后讓這部分?jǐn)?shù)據(jù)可以在所有地方可以被訪問(wèn)到。
所以似乎我們經(jīng)常將這些服務(wù)端數(shù)據(jù)當(dāng)成客戶端狀態(tài)來(lái)看待,除了這些服務(wù)端數(shù)據(jù)(比如:一個(gè)文章列表,你需要顯示的某個(gè)用戶的詳細(xì)信息,...),你的應(yīng)用并不真正擁有它。我們只是借用了最新版本的一份數(shù)據(jù)然后展示給用戶。服務(wù)端才真正擁有這部分?jǐn)?shù)據(jù)。
對(duì)于我來(lái)說(shuō),這給了我一個(gè)如何看待數(shù)據(jù)的新的思路。如果我們能利用緩存來(lái)顯示我們不擁有的那部分?jǐn)?shù)據(jù),那么剩下的應(yīng)用需要處理的真正的客戶端狀態(tài)將大大減少。這使我理解了為什么很多人認(rèn)為Apollo可以在很多場(chǎng)景替代redux。
React Query
我一直沒(méi)有機(jī)會(huì)使用GraphQL。我們有現(xiàn)成的REST API,并沒(méi)有遇到冗余請(qǐng)求的問(wèn)題,目前完全溝通。并沒(méi)有足夠的理由讓我們轉(zhuǎn)換到GraphQL,特別是你還需要讓后端服務(wù)配合進(jìn)行改動(dòng)。
但是我還是羨慕GraphQL帶來(lái)的前端數(shù)據(jù)請(qǐng)求(包括loading和錯(cuò)誤態(tài)的處理)的簡(jiǎn)潔。如果React生態(tài)中有相似的針對(duì)REST API的方案就好了。
讓我們來(lái)看看React Query吧。
由Tanner Linsley在2019年開(kāi)發(fā)的React Query使得在REST API中也可以使用到Apollo所帶來(lái)的好處。他支持任何返回Promise的函數(shù)并且使用了stale-while-revalidate
緩存策略。庫(kù)本身有一些默認(rèn)行為可以盡可能保證數(shù)據(jù)的實(shí)時(shí)性,同時(shí)盡可能快的將數(shù)據(jù)返回給用戶,讓人們感覺(jué)近乎實(shí)時(shí)的體驗(yàn)以提供優(yōu)秀的用戶體驗(yàn)。在這之上,他同時(shí)提供了靈活的自定義能力來(lái)滿足各種場(chǎng)景。
這篇文章并不會(huì)對(duì)React Query進(jìn)行詳細(xì)的介紹。
我認(rèn)為官方文檔已經(jīng)對(duì)使用和概念進(jìn)行了很好的介紹,同時(shí)也有很多關(guān)于這方面的視頻,并且Tanner開(kāi)了一門(mén)課程可以讓你熟悉這個(gè)庫(kù)。
我將會(huì)更多的關(guān)注在官方文檔之外的一些實(shí)踐上的介紹,當(dāng)你已經(jīng)使用這個(gè)庫(kù)一段時(shí)間之后,也許這些介紹對(duì)你會(huì)有所幫助。這其中有一些我過(guò)去幾個(gè)月在深度使用React Query以及從React Query社區(qū)中總結(jié)出的經(jīng)驗(yàn)。
關(guān)于默認(rèn)行為的解釋
我相信React Query的默認(rèn)行為是經(jīng)過(guò)深思熟慮的,但是他們有時(shí)會(huì)讓你措手不及,特別是剛開(kāi)始使用的時(shí)候。
首先,React Query并不會(huì)在每次render的時(shí)候都執(zhí)行queryFn
,即使默認(rèn)的staleTime
是0。你的應(yīng)用在任何時(shí)候可能會(huì)因?yàn)楦鞣N原因重新render,所以如果每次都fetch是瘋狂的!
如果你看到了一個(gè)你不希望的refetch,這很可能是因?yàn)槟銊偩劢沽水?dāng)前窗口同時(shí)React Query執(zhí)行了refetchOnWindowFocus
,這在生產(chǎn)環(huán)境是一個(gè)很棒的特性:如果用戶在不同的瀏覽器tab之間切換,然后回到了你的應(yīng)用,一個(gè)后臺(tái)的refetch會(huì)被自動(dòng)觸發(fā),如果在同一個(gè)時(shí)間服務(wù)端數(shù)據(jù)發(fā)生了變更,那屏幕上的數(shù)據(jù)會(huì)被更新。所有這些會(huì)在看不到loading態(tài)的情況下發(fā)生,如果數(shù)據(jù)和緩存中的數(shù)據(jù)對(duì)比沒(méi)有變化的話,你的組件不會(huì)進(jìn)行重新render。
在開(kāi)發(fā)階段,這個(gè)現(xiàn)象會(huì)出現(xiàn)得更加頻繁,特別是當(dāng)你在瀏覽器DevTools和你的應(yīng)用之間切換的時(shí)候。
其次,cacheTime
和staleTime
的區(qū)別似乎經(jīng)常讓人感到困惑,所以讓我來(lái)說(shuō)明一下:
- StaleTime:一個(gè)查詢變成失效之前的時(shí)長(zhǎng)。如果查詢是有效的,那么查詢就會(huì)一直使用緩存中的數(shù)據(jù),不會(huì)進(jìn)行網(wǎng)絡(luò)請(qǐng)求。如果查詢是處于失效狀態(tài)(默認(rèn)情況下查詢會(huì)立即失效),首先仍然會(huì)從緩存中獲取數(shù)據(jù),但是同時(shí)后臺(tái)在滿足一定條件的情況下會(huì)發(fā)起一次查詢請(qǐng)求。
- CacheTime:查詢從變成非激活態(tài)到從緩存中移除持續(xù)的時(shí)長(zhǎng)。默認(rèn)是五分鐘,當(dāng)沒(méi)有注冊(cè)的觀察者的時(shí)候,查詢會(huì)變成非激活態(tài),所以如果所有使用了某個(gè)查詢的組件都銷(xiāo)毀的時(shí)候,這個(gè)查詢就變成了非激活態(tài)。
大多數(shù)情況下,如果你要改變這兩個(gè)設(shè)置其中的某一個(gè)的話,大部分情況下應(yīng)該修改staleTime
。我很少會(huì)需要修改cacheTime
。在文檔里面也有一個(gè)關(guān)于這個(gè)的解釋。
使用React Query DevTools
DevTools會(huì)幫助你更好的理解查詢中的狀態(tài)。它會(huì)告訴你當(dāng)前緩存中的數(shù)據(jù)是什么,所以你可以更方便的進(jìn)行調(diào)試。除了這些,我發(fā)現(xiàn)在DevTools中可以模擬你的網(wǎng)絡(luò)環(huán)境來(lái)更直觀的看到后臺(tái)refetch,因?yàn)楸镜胤?wù)一般都很快。
把query key理解成一個(gè)依賴列表
我這里所說(shuō)的依賴列表是類(lèi)比useEffect
中說(shuō)到的依賴列表,我假設(shè)你已經(jīng)對(duì)useEffect
已經(jīng)比較熟悉了。
為什么這兩者會(huì)是相似的呢?
因?yàn)镽eact Query會(huì)觸發(fā)refetch當(dāng)query key發(fā)生變化。所以當(dāng)我們給queryFn
傳了一個(gè)變量的時(shí)候,大部分情況下我們都是希望當(dāng)這個(gè)變量發(fā)生變化的時(shí)候可以請(qǐng)求數(shù)據(jù)。相比于通過(guò)復(fù)雜的代碼邏輯來(lái)手動(dòng)觸發(fā)一個(gè)refetch,我們可以利用query key:
type State = 'all' | 'open' | 'done' type Todo = { id: number state: State } type Todos = ReadonlyArray<Todo> const fetchTodos = async (state: State): Promise<Todos> => { const response = await axios.get(`todos/${state}`) return response.data } export const useTodosQuery = (state: State) => useQuery(['todos', state], () => fetchTodos(state))
這里,想象我們的UI顯示了一個(gè)帶有過(guò)濾器的todo列表。我們會(huì)有一些本地狀態(tài)來(lái)存儲(chǔ)過(guò)濾器的數(shù)據(jù),當(dāng)用戶改變了過(guò)濾條件之后,我們會(huì)更新本地的狀態(tài),然后React Query會(huì)自動(dòng)觸發(fā)一個(gè)refetch,因?yàn)閝uery key發(fā)生了變化。我們最終實(shí)現(xiàn)了過(guò)濾狀態(tài)和查詢函數(shù)的同步,這與useEffect中的依賴列表很相似。我從來(lái)沒(méi)有沒(méi)有出現(xiàn)過(guò)給queryFn
傳了一個(gè)變量,但是這個(gè)變量不是queryKey
的一部分的情況。
一個(gè)新的緩存入口
因?yàn)閝uery key被用作緩存的key,所以當(dāng)你把狀態(tài)從all改成done的時(shí)候,你會(huì)得到一個(gè)新的緩存入口,當(dāng)你第一次切換過(guò)濾狀態(tài)的時(shí)候,會(huì)導(dǎo)致一個(gè)強(qiáng)制的loading狀態(tài)(很可能會(huì)限制一個(gè)loading動(dòng)畫(huà))。這當(dāng)然不是最理想的,所以你可以使用keepPreviousData
來(lái)處理這種情況,或者你可以使用initialData來(lái)為新的緩存入口預(yù)填充數(shù)據(jù)。上面那個(gè)例子可以很完美的解釋這個(gè)情況,因?yàn)槲覀兛梢宰鲆恍┛蛻舳说臄?shù)據(jù)預(yù)過(guò)濾:
type State = 'all' | 'open' | 'done' type Todo = { id: number state: State } type Todos = ReadonlyArray<Todo> const fetchTodos = async (state: State): Promise<Todos> => { const response = await axios.get(`todos/${state}`) return response.data } export const useTodosQuery = (state: State) => useQuery(['todos', state], () => fetchTodos(state), { initialData: () => { const allTodos = queryClient.getQueryData<Todos>(['todos', 'all']) const filteredData = allTodos?.filter((todo) => todo.state === state) ?? [] return filteredData.length > 0 ? filteredData : undefined }, })
現(xiàn)在,每次用戶切換過(guò)濾條件的時(shí)候,如果我們沒(méi)有數(shù)據(jù),我們會(huì)嘗試用'all todos'緩存中的數(shù)據(jù)來(lái)預(yù)填充。我們可以馬上就顯示'done'的todo給用戶,他們可以在后臺(tái)fetch結(jié)束之后看到更新之后的列表。注意v3版本中,你需要設(shè)置initialStale
屬性來(lái)觸發(fā)一個(gè)后臺(tái)fetch。
我認(rèn)為這簡(jiǎn)單的幾行代碼可以給你帶來(lái)很好的用戶體驗(yàn)的提升。
把服務(wù)端狀態(tài)和客戶端狀態(tài)分開(kāi)
這個(gè)觀點(diǎn)和我上個(gè)月寫(xiě)的文檔一樣:如果你從useQuery
中拿到了數(shù)據(jù),不要把這部分?jǐn)?shù)據(jù)放到本地狀態(tài)中。主要的原因是這樣會(huì)使得React Query所有后臺(tái)更新失效,因?yàn)閺?fù)制出來(lái)的本地狀態(tài)不會(huì)自動(dòng)更新。
如果你希望獲取一些默認(rèn)數(shù)據(jù)來(lái)設(shè)置一個(gè)表單的默認(rèn)值,然后使用數(shù)據(jù)來(lái)渲染表單,那是可以的。后臺(tái)更新并不會(huì)因?yàn)楸韱我呀?jīng)初始化就忽略之后更新的數(shù)據(jù)。所以如果你想打到這個(gè)目的,確保通過(guò)設(shè)置staleTime
來(lái)避免觸發(fā)不必要的后臺(tái)refetch:
const App = () => { const { data } = useQuery('key', queryFn, { staleTime: Infinity }) return data ? <MyForm initialData={data} /> : null } const MyForm = ({ initialData} ) => { const [data, setData] = React.useState(initialData) ... }
enabled屬性是很強(qiáng)大的
useQuery
hook有很多屬性可以用來(lái)自定義他的行為,enabled
屬性是很強(qiáng)大的一個(gè),它可以讓你做很多有意思的事情。下面是一些我們可以利用它來(lái)實(shí)現(xiàn)的功能:
在一個(gè)查詢中獲取數(shù)據(jù),然后第二個(gè)查詢只有當(dāng)我們成功的從上一個(gè)查詢中獲取數(shù)據(jù)的時(shí)候才會(huì)觸發(fā)
- 開(kāi)啟/關(guān)閉查詢
假設(shè)我們有一個(gè)定時(shí)查詢,通過(guò)refetchInterval
來(lái)實(shí)現(xiàn),但是當(dāng)一個(gè)彈窗打開(kāi)的時(shí)候我們可以暫停這個(gè)查詢,避免彈窗后面的內(nèi)容發(fā)生變更。
- 等待用戶輸入
比如我們有一些過(guò)濾條件作為query key,但是當(dāng)用戶還沒(méi)進(jìn)行過(guò)濾操作的時(shí)候可以不進(jìn)行查詢。
不要把queryCache當(dāng)成本地狀態(tài)管理器
如果你要修改queryCache,它應(yīng)該只發(fā)生在樂(lè)觀更新或者在變更之后拿到后臺(tái)返回的新數(shù)據(jù)的時(shí)候。記住任何一個(gè)后臺(tái)refetch都會(huì)覆蓋這些數(shù)據(jù),所以可以使用其他本地狀態(tài)管理庫(kù)
創(chuàng)建自定義hook
即使你只是封裝一個(gè)useQuery
調(diào)用,創(chuàng)建一個(gè)自定義hook通常情況下也是值得的,因?yàn)椋?/p>
- 你可以把真實(shí)的數(shù)據(jù)獲取邏輯和UI分離,當(dāng)時(shí)把它和useQuery調(diào)用封裝在一起
- 你可以把對(duì)于某個(gè)query key的使用都放在同一個(gè)文件里面
- 如果你需要修改一些設(shè)置或者增加一些數(shù)據(jù)轉(zhuǎn)換邏輯,你可以在一個(gè)地方進(jìn)行
在上面的todo例子里面已經(jīng)有一些使用場(chǎng)景。
我希望這些實(shí)踐經(jīng)驗(yàn)可以幫助你熟悉React Query,更多關(guān)于React Query 實(shí)踐的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react實(shí)現(xiàn)數(shù)據(jù)監(jiān)聽(tīng)方式
這篇文章主要介紹了react實(shí)現(xiàn)數(shù)據(jù)監(jiān)聽(tīng)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08React關(guān)于antd table中select的設(shè)值更新問(wèn)題
這篇文章主要介紹了React關(guān)于antd table中select的設(shè)值更新問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03react之umi配置國(guó)際化語(yǔ)言locale的踩坑記錄
這篇文章主要介紹了react之umi配置國(guó)際化語(yǔ)言locale的踩坑記錄,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02在react-router4中進(jìn)行代碼拆分的方法(基于webpack)
這篇文章主要介紹了在react-router4中進(jìn)行代碼拆分的方法(基于webpack),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-03-03React純前端模擬實(shí)現(xiàn)登錄鑒權(quán)
這篇文章主要為大家詳細(xì)介紹了React純前端模擬實(shí)現(xiàn)登錄鑒權(quán)的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-04-04理解react中受控組件和非受控組件及應(yīng)用場(chǎng)景
當(dāng)涉及到React框架時(shí),了解受控組件和非受控組件是非常重要的概念,本文主要介紹了理解react中受控組件和非受控組件及應(yīng)用場(chǎng)景,具有一定的參考價(jià)值,感興趣的可以了解一下2024-01-01