前言
先前文章提到:如何優雅的於 Vue 正確的處理資料載入點出了一些獲取外部資料延伸帶來的問題與實際解方,這次我想透過 TanStack Query 的視角挖掘怎麼建構更好的前端資料獲取體驗。
前端面對「伺服器狀態」
- 遠端持久化到無法控制或擁有的位置
- 需要異步 API 來獲取和更新數據
- 共享所有權,可能會在不知情的情況下改動,不小心會「過期」
前端需要頻繁的與「伺服端狀態 Server State」打交道,過程中對於狀態的獲取、管理、同步與快取都是讓人頭痛的問題。實際工作規模通常會遇到「滿天飛的狀態」與「重複邏輯」,八成都與伺服端狀態脫不了關係。
必須說我的前端生涯花費太多時間處理與理解瑣碎的伺服端狀態了,期望使用恰當的 TanStack Query 抽象,可以極大改善非同步狀態開發體驗,甚至用戶體驗!
TanStack Query 解決什麼問題?
- 自動快取 + 自動重新取得(Stale While Revalidate、視窗重新聚焦、輪詢)
- 平行 + 依賴請求
- Mutations(Server side-effect)
- 多層快取 + 自動垃圾回收
- 分頁 + Cursor-based 查詢
- 無限載入 Query
- 請求取消
- 專用開發者工具
像是進階一點的元框架如:Next 或 Nuxt 會有既定的方式與整合好的資料獲取方案希望你採用。而 TanStack Query 是一款輕巧支援所有熱門前端框架的非同步狀態管理方案,從以下簡單的範例與三個概念開始:useQuery
、queryKey
、queryFn
。
<script setup>import { useQuery } from '@tanstack/vue-query'
const { isPending, isFetching, isError, data, error } = useQuery({ queryKey: ['todos'], queryFn: getTodos,})</script>
const fetchTodos = (): Promise<Todo[]> => axios.get('/todos').then((response) => response.data)
自動快取與資料同步
每一次的 API 請求都會被自動快取,並根據 queryKey
自動識別與合併。當在不同元件中使用相同的 queryKey
查詢只會執行一次網路請求並共享結果。這也意味著切換頁面時無需重新請求,節省資源與提升用戶體驗。
背景更新
Stale-While-Revalidate(SWR):「先用舊的資料馬上回應使用者,同時在背景中更新新的資料,下一次再用新的資料。」
資料可能隨時在伺服器端被其他使用者或行為更新,TanStack Query 支援自動或手動觸發「背景更新」,讓 UI 保持最新資訊但不打斷用戶操作流程。例如滑動列表時不會閃爍,但背景已經默默更新完畢。
過期邏輯
const { isPending, isFetching, isError, data, error } = useQuery({ queryKey: ['todos'], queryFn: getTodos, staleTime: 60 * 1000, // 1 minute})
可以精細控制資料何時被視為「過期」,透過 staleTime 參數告訴系統在多久之內資料算是有效的,避免每次 render 都觸發 API。幫助在「資料即時性」與「效能」之間找到平衡。
並行 Fetching
原生 JS 除非使用 Promise.all
否則 async
await
使用上實際是串行執行,而 TanStack Query 預設最大化並行執行,且提供更方便優雅的方式管理請求狀態:
// Demo-1await fetchUser();await fetchPosts();
// Demo-2await Promise.all([ fetchUser(), fetchPosts(), fetchComments(),]);
// TanStack Queryconst usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams })const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects,})
依賴 Fetching
一個 Query 依賴另外一個 Query 執行也是常有的事,舉例來說需要取得某用戶再取得該用戶的資源:
// Get the userconst { data: user } = useQuery({ queryKey: ['user', email], queryFn: () => getUserByEmail(email.value),})
const userId = computed(() => user.value?.id)const enabled = computed(() => !!user.value?.id)
// Then get the user's projectsconst { isIdle, data: projects } = useQuery({ queryKey: ['projects', userId], queryFn: () => getProjectsByUser(userId.value), enabled, // The query will not execute until `enabled == true`})
執行具副作用行為使用 useMutation
const { mutate, isPending } = useMutation({ mutationFn: async (newTodo) => { const { data } = await axios.post("/todos", newTodo) return data }, onSucess: () => { queryClient.invalidateQueries({ queryKey: ["todos"]}) }})
相較 useQuery
聲明式的讀取資料,useMutation
命令式的處理副作用(創建、更新、刪除)且可以手動呼叫 invalidateQueries
來讓相關快取失效並自動重新拉資料。這樣不需要手動維護資料同步的邏輯。
總結
基本上 useQuery
、useMutation
和 invalidateQueries
足以應付管理大多 CRUD 應用,一些進階的功能如樂觀更新、分頁都可以到官方文件了解更多。
延伸閱讀
- React Query in 100 Seconds - Fireship
- 深入淺出 TanStack Query(一):在呼叫 useQuery 後發生了什麼事
- 深入淺出 TanStack Query(二):在呼叫 useMutation 後發生了什麼事
- 深入淺出 TanStack Query(三):在呼叫 invalidateQueries 後發生了什麼事
- Vue3 教學 - Vue Query Part.1 使用 useQuery 與 useMutation