React Query 介紹
Redux 與 React Query 應是互補
Redux (或 zustand、jotai 等)專注在複雜或是全域的 client state
- 使用者偏好設定、是否打開 modal、具有強大編輯功能的元件狀態...等
React Query 專注在 server state 管理,而 server state 管理的需求是一般狀態管理工具難以辦到的:
- 緩存機制
- 將多個相同 request 合併為單一 request (dedupe)
- 背景更新「不新鮮」資料
- 管理資料的「新鮮度」
- 緩存回收
- 透過結構達到資料共享
預想的使用方式可能是:
- 單純呈現 server 資料(例如項目列表、搜尋結果): react query
- 單純修改 server 資料(POST、DELETE 等): react query
- 修改 server 資料後,再 GET:react query
- 需要先拉取 sever 資料,再到 client side 做複雜編輯:react query 拉取,使用
useEffect
dispatch 更新到 redux
使用 React Query 的基本要求
丟進去 useQuery
或 useMutation
的 function,能回傳一個 Promise 就可以了,api service 怎麼封裝都可以兼容,頗讚
note: api service 指的是把 axios 所需的基礎 config 和攔截器進行封裝,並抽象化成函數,只保留所需參數,例如 login()
、getPosts()
(RTK Query 和封裝完畢的 api service 向性滿不好的)
export function useUserInfo() {
const isLogin = useSelector(isLoginSelector);
const query = useQuery({
queryKey: [API_SERVICES.GET_USER_INFO],
// 一個回傳 Promise 的函數就可以
// 其他比較複雜的 GET 可以從 hook 本身設定動態參數傳進去
// ex: () => authApi.getUserInfo({reqBody, queryParams})
queryFn: () => authApi.getUserInfo(),
// promise 成功後的 selector, 可以從這裡做單元測試
// 或許也可以用 createSelector
select: (result) => result?.data,
// 什麼情況才開啟自動 fetch & refetch 機制
enabled: isLogin,
});
return {
query,
data: query.data,
status: query.status,
isError: query.isError,
isSuccess: query.isSuccess,
isPending: query.isPending,
};
}
在這情況下,有使用 useUserInfo
的不同元件,可以有緩存共享機制(因為有一樣的 query key)詳情會後面提到
Query Key
query key
是 useQuery
的關鍵概念,具有以下作用:
- unique 識別:
query key
是一個在 React Query 內部用來追蹤 query 的識別標籤- 可以是一個字符串或一個由 多個值組成的陣列 (建議統一使用陣列)
- React Query 使用這個 key 來存取和緩存資料,確保不同元件使用相同的 key 時,都能獲得相應的資料或狀態
- 資料緩存:
- 當一個 query 被執行後,其結果會被緩存,並與其
query key
關聯 - 如果這個 query 再次被觸發且
query key
未改變,React Query 會從緩存中提取,而不是重新從伺服器拉取,這大大提高了加載效率,詳情見 React Query 的緩存機制 - stale time vs. cache (gc) time
- 當一個 query 被執行後,其結果會被緩存,並與其
- query 無效化和更新:
- 允許你透過
query key
無效化和重新取得某個 query 的資料 - 例如,進行了更新操作後,可能想要立即更新緩存中的資料
- 這時,你可以使用
query key
快速定位該筆資料,並更新或無效化對應的緩存,使之 refetch
- 允許你透過
- dependency 追蹤:
- 如果你的資料取得依賴於特定的變數(例如 query string、state),可以將這些變數包含在
query key
中 - 這樣,當這些依賴變化時,React Query 會自動識別出
query key
的變更,並重新取得資料
- 如果你的資料取得依賴於特定的變數(例如 query string、state),可以將這些變數包含在
可能的 query key
example:
['todos', 'list', { filters: 'all' }]
['todos', 'list', { filters: 'done' }]
['todos', 'detail', 1]
['todos', 'detail', 2]
// ✅ just invalidate all the lists
queryClient.invalidateQueries({
queryKey: ['todos', 'list']
})
良好的 pattern:
統一使用陣列格式,並建立 key factory 來生成 query key
const todoKeys = {
all: ['todos'] as const,
lists: () => [...todoKeys.all, 'list'] as const,
list: (filters: string) => [...todoKeys.lists(), { filters }] as const,
details: () => [...todoKeys.all, 'detail'] as const,
detail: (id: number) => [...todoKeys.details(), id] as const,
}
可以參考 react query 維護者的文章:Effective React Query Keys | TkDodo's blog
useQuery
或 useMutation
經驗法則:
- Read:
useQuery
- Create/Update/Delete:
useMutation
react-query - What's the difference between useQuery and useMutation hook? - Stack Overflow
useQuery
- 宣告式的概念,會有自動執行和緩存的機制,會在元件掛載時就建立 observer
- 有緩存機制:gc time 和 stale time,可以依照情境智慧地 refetch 和使用緩存,提昇效率與體驗
- 承上,因此官方在 v5 拔掉了
onSuccess
等 callback,避免不必要的誤用(refetch 機制會讓這 callback 有很多非預期狀況) - 調用
useQuery
時, 如果 使用同一組 query key 的話,會盡量使用 cache,而不會重複 fetch API useQuery
透過自動 refetch 和緩存,和 API 建立一個類似同步機制的流程,更為「reactive」,所以較適合 GET(不會修改伺服器資料)
useMutation
- 指令式的概念,要調用
mutation
才 fetch API - 因為是使用者主動發出動作才觸發,所以還是保留
onSuccess
、onError
等 callback useMutation
像是一個口令一個動作,適合主動觸發的 POST、DELETE、PUT、PATCH(極可能會修改伺服器資料的 Method)- 使用 redux thunk 比較像是這種概念
- 指令式的概念,要調用
特別狀況:使用 useQuery 來進行 POST
依照上面的經驗法則,POST 大多使用 useMutation
但是如果碰到了「path 含 token 的重設密碼頁面」,需要先驗證 token 才顯示重設密碼表單,這時我覺得很適合用 useQuery
因為驗證 token 也不會改變伺服器資料,所以可以放心使用 useQuery
- 在 mount 階段就發送 API,不用等到 useEffect 階段在去 mutate
- 會有
refetchOnWindowFocus
功能,重新 focus 此分頁就去重新 fetch,確保驗證狀態是最新的,減少送出後才發現 token 失效的情況
情境題:拿 POST 回傳的結果更新 Query
在 useMuation
的 onSuccess
,使用 queryClient.setQueryData
Updates from Mutation Responses | TanStack Query React Docs
情境題:根據第一個 GET 結果,發起第二個 GET
import { useQuery } from 'react-query';
const fetchUser = async () => {
const response = await fetch('/api/user');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
};
const fetchUserDetails = async (userId) => {
const response = await fetch(`/api/user/${userId}/details`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
};
function MyComponent() {
// 第一次請求:取得使用者基本資訊
const { data: user, status: userStatus } = useQuery('user', fetchUser);
// 第二次請求:使用從第一次請求中獲得的使用者 ID
const { data: userDetails, status: userDetailsStatus } = useQuery(
['userDetails', user?.id],
() => fetchUserDetails(user.id),
{
// 只有當 user 有值,且 user.id 也有值時才執行此查詢
enabled: !!user?.id
}
);
// 渲染組件
if (userStatus === 'loading' || userDetailsStatus === 'loading') {
return <div>Loading...</div>;
}
if (userStatus === 'error' || userDetailsStatus === 'error') {
return <div>Error loading data</div>;
}
return (
<div>
<h1>{user.name}</h1>
<p>Details: {userDetails.bio}</p>
</div>
);
}