Skip to main content

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 的基本要求

丟進去 useQueryuseMutation 的 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 keyuseQuery 的關鍵概念,具有以下作用:

  • 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 key 無效化和重新取得某個 query 的資料
    • 例如,進行了更新操作後,可能想要立即更新緩存中的資料
    • 這時,你可以使用 query key 快速定位該筆資料,並更新或無效化對應的緩存,使之 refetch
  • dependency 追蹤
    • 如果你的資料取得依賴於特定的變數(例如 query string、state),可以將這些變數包含在 query key
    • 這樣,當這些依賴變化時,React Query 會自動識別出 query key 的變更,並重新取得資料

可能的 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

useQueryuseMutation

經驗法則:

  • 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
    • 因為是使用者主動發出動作才觸發,所以還是保留 onSuccessonError 等 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

useMuationonSuccess,使用 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>
);
}

其他參考資料

Inside React Query | TkDodo's blog