使用 TanStack Query 輕鬆處理 HTTP Requests
TanStack Query 可以幫助我們發送 HTTP Request 也就是串接 API,讓前端畫面與後端資料能夠溝通與同步。沒錯!這些事情透過 useEffect 以及 Fetch 或 Axios 就能做到了,只是 TanStack Query 大幅地簡化了這方面的 Code,一起來看看 TanStack Query 如何提升開發者體驗吧。
什麼是 TanStack Query
在專案中,我們可以撰寫一個 useHttp
的 Custom Hook,來幫助自己複用 Try & Catch Error 與 Loading 等等操作,不過儘管我們花時間處理,仍會有些漏網之魚的功能或者小錯誤,而這些網路請求的處理全部都已經包裝在 TanStack Query 裡面,這大幅提升了 Developer Experience (DX),讓身為開發者的我們生活得更加 Chill~
除此之外,TanStack Query 也包含了許多咱們 Developer 親自實作會相當複雜而且花時間的功能,例如:緩存功能 (Caching)。當使用者透過 Router 切換至新的畫面後,使用者返回前一個畫面可以不用再次獲取所有的資料,而是先使用 Web 記憶體中的儲存的資料,並且在背後撈取 API 再更新資料,這一切都已經由 TanStack Query 幫我們處理好了。
這麼香的東西,不裝嗎 d(`・∀・)b
安裝套件與配置 QueryClient
1npm i @tanstack/react-query 2
安裝完成後,我們只需要再做兩個步驟,就能完成 TanStack Query 的設定並解鎖所有功能了。
首先是透過 QueryClient
建立一個新的 Client 的實例 (Instance),接著在要使用 TanStack Query 的元素外層包上 <QueryClientProvider>
,通常會在 <App>
裡面包裝。
1import { 2 useQueryClient, 3 QueryClient, 4 QueryClientProvider, 5} from '@tanstack/react-query'; 6 7// Create a client 8const queryClient = new QueryClient(); 9 10function App() { 11 return ( 12 // Provide the client to your App 13 <QueryClientProvider client={queryClient}> 14 <Example /> 15 </QueryClientProvider> 16 ); 17} 18
useQuery Hook
首先介紹 TanStack Query 的 useQuery
Hook,它會回傳該 Request 的 Data、Loading 與 Error 等資訊。
useQuery 的參數
queryKey
:在陣列中放入一個 Key(不一定是字串,也可以是物件),Key 是用來記錄與辨識這個 HTTP Request 是哪個 API 請求的,由於 API 回傳結果會被 Cached,如果發送的是重複的 API Request 就會先使用過去儲存的回應結果queryFn
:放上一個 Promise 函式,像是使用 Fetch API 或者 Axios 撈取資料的函式staleTime
:同一個 Request 需要間隔的時間,預設為 0(例如:設定為 5000 就是在 5 秒內重複呼叫的話,第二次 API 不會被執行)gcTime
:Garbage Collection Time,也就是緩存內容存留的時間,預設為 5 分鐘(例如:設定為 30000 就是半分鐘,也就是 30 秒後緩存的資料就會被丟棄)enabled
:為 API 查詢加上條件判斷,例如:searchTerm !== undefined
得到true
代表可以執行 API 呼叫,反之如果searchTerm
為undefined
的時候就會禁止使用這個 Query
useQuery 的回傳結果
可使用解構方式提取值
data
:API 的回傳資料,一開始會回傳 Cached 的資料,等到 API 真正撈取後會在背景裡更新為新資料isPending
:Request 是否還在進行中,是否已經取得了 Response 資料isLoading
:與isPending
類似,差異在於當此 Query 為 Disabled 狀態時,isLoading
會是false
,而isPending
仍為true
isError
:Response 是否有錯誤error
:錯誤訊息
範例:
1import { useQuery } from '@tanstack/react-query'; 2 3function Example() { 4 const { data, isLoading, isError, error } = useQuery({ 5 queryKey: ['exampleData'], 6 queryFn: async () => { 7 const response = await fetch( 8 'https://api.github.com/repos/TanStack/query', 9 ); 10 return await response.json(); 11 }, 12 }); 13 14 if (isLoading) return 'Loading...'; 15 16 if (isError) return 'An error has occurred: ' + error.message; 17 18 return ( 19 <div> 20 <h1>{data.full_name}</h1> 21 <p>{data.description}</p> 22 <strong>👀 {data.subscribers_count}</strong>{' '} 23 <strong>✨ {data.stargazers_count}</strong>{' '} 24 <strong>🍴 {data.forks_count}</strong> 25 </div> 26 ); 27} 28 29export default Example; 30
如何為 queryFn 帶上參數
在 queryFn
中,其實 TanStack Query 為它默認自帶一個物件的參數,你可以從中拿到一些資訊,例如: signal
,它可以用在 Fetch 或 Axios 上面,透過 await axios(url, { signal: signal })
等方式給予當下是否中止 API 呼叫的訊號。
如果想要給 queryFn
的函式傳參數,必須在函式帶上「具名」的參數,除了原有的 signal
以外,再加上自己要做為參數傳遞的內容,例如: searchTerm
,透過 searchTerm 我們就可以為 API 呼叫帶上參數了。
1const { data, isPending, isError, error } = useQuery({ 2 queryKey: ['exampleData', { search: searchTerm }], 3 queryFn: ({ signal }) => fetchData({ signal, searchTerm }), 4}); 5
useMutation Hook
剛才介紹的 useQuery
是在取得資料,當我們想要做的是更新資料的操作就可以使用 useMutation
Hook。
useMutation 的參數
mutationFn
:跟queryFn
不同,就算需要傳參數mutationFn
也不用透過匿名函式進行包裝onSuccess
:放一個匿名函式,在成功後執行裡面的動作
useMutation 的回傳結果
- 一樣有
isPending
、isError
、error
等資料可以使用 mutate
:我們會用這個屬性來實際發送請求
範例:
1import { useNavigate } from 'react-router-dom'; 2// 引入 useMutation Hook 3import { useMutation } from '@tanstack/react-query'; 4 5function NewEvent() { 6 const navigate = useNavigate(); 7 8 // 使用 useMutation Hook 9 const { mutate, isPending, isError, error } = useMutation({ 10 mutationFn: createNewEvent, 11 onSuccess: () => { 12 navigate('/home'); 13 }, 14 }); 15 16 // 將資料傳送給後端 17 function handleSubmit(formData) { 18 mutate({ event: formData }); // 這個 { event: formData } 參數是依照 API 要求的 Payload 格式 19 } 20 21 return ( 22 <> 23 <form onSubmit={handleSubmit}> 24 <h1>New Event</h1> 25 {isPending && <p>Submitting...</p>} 26 {!isPending && ( 27 <> 28 <button type="button">Cancel</button> 29 <button type="submit">Create</button> 30 </> 31 )} 32 {isError && <p>Failed to create event.</p>} 33 </form> 34 </> 35 ); 36} 37 38export default Example; 39
QueryClient - Invalidate Queries
當在執行 useMutation
的 onSuccess
時,我們還可以透過 queryClient.invalidateQueries()
幫助我們告訴特定 queryKey
的 useQuery
它獲取的 data
已經是過時的,以要求它馬上進行更新。
1const { mutate, isPending, isError, error } = useMutation({ 2 mutationFn: createNewEvent, 3 onSuccess: () => { 4 queryClient.invalidateQueries({ queryKey: ['exampleData'] }); 5 navigate('/home'); 6 }, 7}); 8
Optimistic Updating - onMutate + onError + onSettled
到這邊,我們已經可以透過執行 mutate
去更新資料,並且在完成後透過 onSuccess
再次更新畫面,在這個過程中我們通常會加上一個 Loading 效果以等待所有操作完成。然而,在 UX 上我們還有一個更好的做法,那就是 Optimistic Updating。
簡單來說,就是在 API 資料回傳前,先本地更新畫面,讓使用者更快地看見更新結果,API 則在背景中運行。
在 TanStack Query 中,onMutate
就是指開始更新資料,但是尚未完成的時間點。因此,我們製作 Optimistic Updating 的方式,就是在 onMutate
時透過 queryClient.setQueryData()
去手動更新已緩存的 Query 資料。
另外,這邊還會先使用 queryClient.cancelQueries()
確保取消關於該 Key 的 useQuery
動作,再去進行 Optimistic Updating。
1const { mutate, isPending, isError, error } = useMutation({ 2 mutationFn: updateEventData, 3 onMutate: async (data) => { 4 const newEvent = data.event; 5 await queryClient.cancelQueries({ queryKey: ['events', params.id] }); // 先確保取消所有此 Key 的查詢 6 queryClient.setQueryData(['events', params.id], newEvent); // 手動更新此 Key 的緩存 7 }, 8}); 9
更完整一點的寫法,我們還可以在 onMutate
中透過 queryClient.getQueryData()
取得緩存中的前一個結果並且放在 return
中回傳,這個回傳資料可以在 onError
的 context
中拿到,我們可以把這個東西用來當作錯誤情況的退路。
最後的最後,我們在 onSettled
再加上 invalidateQueries
,這麼做的用意,就是告訴 TanStack Query 不管我樂觀更新是成功或是失敗,都要再次重新查詢一次,確保後端資料庫的內容與前端畫面是一致的。
1const { mutate, isPending, isError, error } = useMutation({ 2 mutationFn: updateEventData, 3 onMutate: async (data) => { 4 const newEvent = data.event; 5 6 await queryClient.cancelQueries({ queryKey: ['events', params.id] }); 7 const previousEvent = queryClient.getQueryData(); // 更新前的資料 8 9 queryClient.setQueryData(['events', params.id], newEvent); 10 11 return { previousEvent }; // 簡潔寫法 12 }, 13 onError: (error, data, context) => { 14 queryClient.setQueryData(['events', params.id], context.previousEvent); // 錯誤處理 15 }, 16 onSettled: () => { 17 queryClient.invalidateQueries({ queryKey: ['events', params.id] }); 18 }, 19}); 20