用React-query解決你一半的狀態(tài)管理問題
按照來源,前端有兩類「狀態(tài)」需要管理:
- 用戶交互的中間狀態(tài)
- 服務(wù)端狀態(tài)
在陳年的老項目中,通常用Redux、Mobx這樣的「全局狀態(tài)管理方案」無差別對待他們。
事實上,他們有很大區(qū)別:
用戶交互的中間狀態(tài)
比如組件的isLoading、isOpen,這類「狀態(tài)」的特點是:
- 以「同步」的形式更新
- 「狀態(tài)」完全由前端控制
- 「狀態(tài)」比較獨立(不同的組件擁有各自的isLoading)
這類「狀態(tài)」通常保存在組件內(nèi)部。
當(dāng)「狀態(tài)」需要跨組件層級傳遞,通常使用Context API。
再大范圍的「狀態(tài)」會使用Redux這樣的「全局狀態(tài)管理方案」。
服務(wù)端狀態(tài)
當(dāng)我們從服務(wù)端請求數(shù)據(jù):
- function App() {
- const [data, updateData] = useState(null);
- useEffect(async () => {
- const data = await axios.get('/api/user');
- updateData(data);
- }, [])
- // 處理data
- }
返回的數(shù)據(jù)通常作為「狀態(tài)」保存在組件內(nèi)部(如App組件的data狀態(tài))。
如果是需要復(fù)用的通用「狀態(tài)」,通常將其保存在Redux這樣的「全局狀態(tài)管理方案」中。
這樣做有2個壞處:
1.需要重復(fù)處理請求中間狀態(tài)
為了讓App組件健壯,我們還需要處理請求中、出錯等中間狀態(tài):
- function App() {
- const [data, updateData] = useState(null);
- const [isError, setError] = useState(false);
- const [isLoading, setLoading] = useState(false);
- useEffect(async () => {
- setError(false);
- setLoading(true);
- try {
- const data = await axios.get('/api/user');
- updateData(data);
- } catch(e) {
- setError(true);
- }
- setLoading(false);
- }, [])
- // 處理data
- }
這類通用的中間狀態(tài)處理邏輯可能在不同組件中重復(fù)寫很多次。
2.「緩存」的性質(zhì)不同于「狀態(tài)」
不同于交互的中間狀態(tài),服務(wù)端狀態(tài)更應(yīng)被歸類為「緩存」,他有如下性質(zhì):
- 通常以「異步」的形式請求、更新
- 「狀態(tài)」由請求的數(shù)據(jù)源控制,不由前端控制
- 「狀態(tài)」可以由不同組件共享
作為可以由不同組件共享的「緩存」,還需要考慮更多問題,比如:
- 緩存失效
- 緩存更新
Redux一把梭固然方便。但是,區(qū)別對待不同類型「狀態(tài)」能讓項目更可控。
這里,推薦使用React-Query管理服務(wù)端狀態(tài)。
另一個可選方案是SWR[1]。你可以從這里[2]看到他們的區(qū)別
初識React-Query
React-Query是一個基于hooks的數(shù)據(jù)請求庫。
我們可以將剛才的例子用React-Query改寫:
- import { useQuery } from 'react-query'
- function App() {
- const {data, isLoading, isError} = useQuery('userData', () => axios.get('/api/user'));
- if (isLoading) {
- return <div>loading</div>;
- }
- return (
- <ul>
- {data.map(user => <li key={user.id}>{user.name}</li>)}
- </ul>
- )
- }
React-Query中的Query指一個異步請求的數(shù)據(jù)源。
例子中userData字符串就是這個query獨一無二的key。
可以看到,React-Query封裝了完整的請求中間狀態(tài)(isLoading、isError...)。
不僅如此,React-Query還為我們做了如下工作:
- 多個組件請求同一個query時只發(fā)出一個請求
- 緩存數(shù)據(jù)失效/更新策略(判斷緩存合適失效,失效后自動請求數(shù)據(jù))
- 對失效數(shù)據(jù)垃圾清理
數(shù)據(jù)的CRUD由2個hook處理:
- useQuery處理數(shù)據(jù)的查
- useMutation處理數(shù)據(jù)的增/刪/改
在下面的例子中,點擊「創(chuàng)建用戶」按鈕會發(fā)起創(chuàng)建用戶的post請求:
- import { useQuery, queryCache } from 'react-query';
- unction App() {
- const {data, isLoading, isError} = useQuery('userData', () => axios.get('/api/user'));
- // 新增用戶
- const {mutate} = useMutation(data => axios.post('/api/user', data));
- return (
- <ul>
- {data.map(user => <li key={user.id}>{user.name}</li>)}
- <button
- onClick={() => {
- mutate({name: 'kasong', age: 99})
- }}
- >
- 創(chuàng)建用戶
- </button>
- </ul>
- )
但是點擊后userData query對應(yīng)數(shù)據(jù)不會更新,因為他還未失效。
所以我們需要告訴React-Query,userData query對應(yīng)的緩存已經(jīng)失效,需要更新:
- import { useQuery, queryCache } from 'react-query';
- function App() {
- // ...
- const {mutate} = useMutation(userData => axios.post('/api/user', userData), {
- onSuccess: () => {
- queryCache.invalidateQueries('userData')
- }
- })
- // ...
- }
通過調(diào)用mutate方法,會觸發(fā)請求。
當(dāng)請求成功后,會觸發(fā)onSuccess回調(diào),回調(diào)中調(diào)用queryCache.invalidateQueries,將userData對應(yīng)的query緩存置為invalidate。
這樣,React-Query就會重新請求userData對應(yīng)query的數(shù)據(jù)。
總結(jié)
通過使用React-Query(或SWR)這樣的數(shù)據(jù)請求庫,可以將服務(wù)端狀態(tài)從全局狀態(tài)中解放出來。
這為我們帶來很多好處:
- 使用通用的hook處理請求中間狀態(tài)
- 多余請求合并
- 針對緩存的更新/失效策略
- Redux等「全局狀態(tài)管理方案」可以更專注于「前端中間狀態(tài)」處理
參考資料
[1]SWR: https://swr.vercel.app/
[2]這里: https://react-query.tanstack.com/comparison
【編輯推薦】