網站首頁 編程語言 正文
引言
當2018年GraphQL特別是Apolllo Client開始流行之后,很多人開始認為它將替代Redux,關于Redux是否已經落伍的問題經常被問到。
我很清晰地記得我當時對這些觀點的不理解。為什么一些數據請求的庫會替代全局狀態管理庫呢?這兩者有什么關聯呢?
曾經我認為像Apollo這樣的Graphql客戶端只能用來請求數據,就像axios一樣,你仍然需要一些方式來讓請求的數據可以被應用程序訪問到。
我發現我大錯特錯。
客戶端狀態 vs 服務端狀態
Apollo提供的不僅僅是描述所需數據同時獲取數據的能力,它同時提供了針對這些服務端數據的緩存能力。這意味著你可以在多個組件中使用相同的useQuery
hook,它只會觸發一次數據請求并且按照請求的先后順序返回緩存中的數據。
這看起來跟我們(包括很多除了我們以外的團隊)在一些場景使用redux
的目的很相似:從服務器獲取數據,然后讓這部分數據可以在所有地方可以被訪問到。
所以似乎我們經常將這些服務端數據當成客戶端狀態來看待,除了這些服務端數據(比如:一個文章列表,你需要顯示的某個用戶的詳細信息,...),你的應用并不真正擁有它。我們只是借用了最新版本的一份數據然后展示給用戶。服務端才真正擁有這部分數據。
對于我來說,這給了我一個如何看待數據的新的思路。如果我們能利用緩存來顯示我們不擁有的那部分數據,那么剩下的應用需要處理的真正的客戶端狀態將大大減少。這使我理解了為什么很多人認為Apollo可以在很多場景替代redux。
React Query
我一直沒有機會使用GraphQL。我們有現成的REST API,并沒有遇到冗余請求的問題,目前完全溝通。并沒有足夠的理由讓我們轉換到GraphQL,特別是你還需要讓后端服務配合進行改動。
但是我還是羨慕GraphQL帶來的前端數據請求(包括loading和錯誤態的處理)的簡潔。如果React生態中有相似的針對REST API的方案就好了。
讓我們來看看React Query吧。
由Tanner Linsley在2019年開發的React Query使得在REST API中也可以使用到Apollo所帶來的好處。他支持任何返回Promise的函數并且使用了stale-while-revalidate
緩存策略。庫本身有一些默認行為可以盡可能保證數據的實時性,同時盡可能快的將數據返回給用戶,讓人們感覺近乎實時的體驗以提供優秀的用戶體驗。在這之上,他同時提供了靈活的自定義能力來滿足各種場景。
這篇文章并不會對React Query進行詳細的介紹。
我認為官方文檔已經對使用和概念進行了很好的介紹,同時也有很多關于這方面的視頻,并且Tanner開了一門課程可以讓你熟悉這個庫。
我將會更多的關注在官方文檔之外的一些實踐上的介紹,當你已經使用這個庫一段時間之后,也許這些介紹對你會有所幫助。這其中有一些我過去幾個月在深度使用React Query以及從React Query社區中總結出的經驗。
關于默認行為的解釋
我相信React Query的默認行為是經過深思熟慮的,但是他們有時會讓你措手不及,特別是剛開始使用的時候。
首先,React Query并不會在每次render的時候都執行queryFn
,即使默認的staleTime
是0。你的應用在任何時候可能會因為各種原因重新render,所以如果每次都fetch是瘋狂的!
如果你看到了一個你不希望的refetch,這很可能是因為你剛聚焦了當前窗口同時React Query執行了refetchOnWindowFocus
,這在生產環境是一個很棒的特性:如果用戶在不同的瀏覽器tab之間切換,然后回到了你的應用,一個后臺的refetch會被自動觸發,如果在同一個時間服務端數據發生了變更,那屏幕上的數據會被更新。所有這些會在看不到loading態的情況下發生,如果數據和緩存中的數據對比沒有變化的話,你的組件不會進行重新render。
在開發階段,這個現象會出現得更加頻繁,特別是當你在瀏覽器DevTools和你的應用之間切換的時候。
其次,cacheTime
和staleTime
的區別似乎經常讓人感到困惑,所以讓我來說明一下:
- StaleTime:一個查詢變成失效之前的時長。如果查詢是有效的,那么查詢就會一直使用緩存中的數據,不會進行網絡請求。如果查詢是處于失效狀態(默認情況下查詢會立即失效),首先仍然會從緩存中獲取數據,但是同時后臺在滿足一定條件的情況下會發起一次查詢請求。
- CacheTime:查詢從變成非激活態到從緩存中移除持續的時長。默認是五分鐘,當沒有注冊的觀察者的時候,查詢會變成非激活態,所以如果所有使用了某個查詢的組件都銷毀的時候,這個查詢就變成了非激活態。
大多數情況下,如果你要改變這兩個設置其中的某一個的話,大部分情況下應該修改staleTime
。我很少會需要修改cacheTime
。在文檔里面也有一個關于這個的解釋。
使用React Query DevTools
DevTools會幫助你更好的理解查詢中的狀態。它會告訴你當前緩存中的數據是什么,所以你可以更方便的進行調試。除了這些,我發現在DevTools中可以模擬你的網絡環境來更直觀的看到后臺refetch,因為本地服務一般都很快。
把query key理解成一個依賴列表
我這里所說的依賴列表是類比useEffect
中說到的依賴列表,我假設你已經對useEffect
已經比較熟悉了。
為什么這兩者會是相似的呢?
因為React Query會觸發refetch當query key發生變化。所以當我們給queryFn
傳了一個變量的時候,大部分情況下我們都是希望當這個變量發生變化的時候可以請求數據。相比于通過復雜的代碼邏輯來手動觸發一個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顯示了一個帶有過濾器的todo列表。我們會有一些本地狀態來存儲過濾器的數據,當用戶改變了過濾條件之后,我們會更新本地的狀態,然后React Query會自動觸發一個refetch,因為query key發生了變化。我們最終實現了過濾狀態和查詢函數的同步,這與useEffect中的依賴列表很相似。我從來沒有沒有出現過給queryFn
傳了一個變量,但是這個變量不是queryKey
的一部分的情況。
一個新的緩存入口
因為query key被用作緩存的key,所以當你把狀態從all改成done的時候,你會得到一個新的緩存入口,當你第一次切換過濾狀態的時候,會導致一個強制的loading狀態(很可能會限制一個loading動畫)。這當然不是最理想的,所以你可以使用keepPreviousData
來處理這種情況,或者你可以使用initialData來為新的緩存入口預填充數據。上面那個例子可以很完美的解釋這個情況,因為我們可以做一些客戶端的數據預過濾:
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 }, })
現在,每次用戶切換過濾條件的時候,如果我們沒有數據,我們會嘗試用'all todos'緩存中的數據來預填充。我們可以馬上就顯示'done'的todo給用戶,他們可以在后臺fetch結束之后看到更新之后的列表。注意v3版本中,你需要設置initialStale
屬性來觸發一個后臺fetch。
我認為這簡單的幾行代碼可以給你帶來很好的用戶體驗的提升。
把服務端狀態和客戶端狀態分開
這個觀點和我上個月寫的文檔一樣:如果你從useQuery
中拿到了數據,不要把這部分數據放到本地狀態中。主要的原因是這樣會使得React Query所有后臺更新失效,因為復制出來的本地狀態不會自動更新。
如果你希望獲取一些默認數據來設置一個表單的默認值,然后使用數據來渲染表單,那是可以的。后臺更新并不會因為表單已經初始化就忽略之后更新的數據。所以如果你想打到這個目的,確保通過設置staleTime
來避免觸發不必要的后臺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屬性是很強大的
useQuery
?hook有很多屬性可以用來自定義他的行為,enabled
屬性是很強大的一個,它可以讓你做很多有意思的事情。下面是一些我們可以利用它來實現的功能:
- 依賴查詢
在一個查詢中獲取數據,然后第二個查詢只有當我們成功的從上一個查詢中獲取數據的時候才會觸發
- 開啟/關閉查詢
假設我們有一個定時查詢,通過refetchInterval
來實現,但是當一個彈窗打開的時候我們可以暫停這個查詢,避免彈窗后面的內容發生變更。
- 等待用戶輸入
比如我們有一些過濾條件作為query key,但是當用戶還沒進行過濾操作的時候可以不進行查詢。
不要把queryCache當成本地狀態管理器
如果你要修改queryCache,它應該只發生在樂觀更新或者在變更之后拿到后臺返回的新數據的時候。記住任何一個后臺refetch都會覆蓋這些數據,所以可以使用其他本地狀態管理庫
創建自定義hook
即使你只是封裝一個useQuery
調用,創建一個自定義hook通常情況下也是值得的,因為:
- 你可以把真實的數據獲取邏輯和UI分離,當時把它和useQuery調用封裝在一起
- 你可以把對于某個query key的使用都放在同一個文件里面
- 如果你需要修改一些設置或者增加一些數據轉換邏輯,你可以在一個地方進行
在上面的todo例子里面已經有一些使用場景。
原文鏈接:https://segmentfault.com/a/1190000042629569
相關推薦
- 2022-07-26 Fatal error in launcher: Unable to create process
- 2022-09-12 Python中.py程序在CMD控制臺以指定虛擬環境運行_python
- 2022-11-03 react+tsx中使用better-scroll詳解_React
- 2023-03-19 Kotlin?WorkManager使用方法詳解_Android
- 2022-02-26 Android操作SQLite基本用法_Android
- 2022-07-11 Linux刪除某個字母開頭的所有文件
- 2022-03-24 基于Docker的可持續交付問題_docker
- 2022-10-12 字節封裝React組件手機號自動校驗格式FormItem_React
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支