Better Server State Management through TanStack Query!

如何透過 TanStack Query 建構更好的前端資料獲取體驗

前言

先前文章提到:如何優雅的於 Vue 正確的處理資料載入點出了一些獲取外部資料延伸帶來的問題與實際解方,這次我想透過 TanStack Query🔗 的視角挖掘怎麼建構更好的前端資料獲取體驗。

前端面對「伺服器狀態」

  • 遠端持久化到無法控制或擁有的位置
  • 需要異步 API 來獲取和更新數據
  • 共享所有權,可能會在不知情的情況下改動,不小心會「過期」

前端需要頻繁的與「伺服端狀態 Server State🔗」打交道,過程中對於狀態的獲取、管理、同步與快取都是讓人頭痛的問題。實際工作規模通常會遇到「滿天飛的狀態」與「重複邏輯」,八成都與伺服端狀態脫不了關係。

必須說我的前端生涯花費太多時間處理與理解瑣碎的伺服端狀態了,期望使用恰當的 TanStack Query 抽象,可以極大改善非同步狀態開發體驗,甚至用戶體驗!

TanStack Query 解決什麼問題?

像是進階一點的元框架如:Next🔗Nuxt🔗 會有既定的方式與整合好的資料獲取方案希望你採用。而 TanStack Query🔗 是一款輕巧支援所有熱門前端框架的非同步狀態管理方案,從以下簡單的範例與三個概念開始:useQueryqueryKeyqueryFn

<script setup>
import { useQuery } from '@tanstack/vue-query'
const { isPending, isFetching, isError, data, error } = useQuery({
queryKey: ['todos'],
queryFn: getTodos,
})
</script>
  • useQuery🔗 為(Composable / Hook) 管理並回傳狀態
  • queryKey🔗 為獨一無二的辨別用字串陣列用於判斷快取與重載入
  • queryFn🔗 為回傳 Promise 的函式
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-1
await fetchUser();
await fetchPosts();
// Demo-2
await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
// TanStack Query
const 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 user
const { 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 projects
const { 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 來讓相關快取失效並自動重新拉資料。這樣不需要手動維護資料同步的邏輯。

總結

基本上 useQueryuseMutationinvalidateQueries 足以應付管理大多 CRUD 應用,一些進階的功能如樂觀更新🔗分頁🔗都可以到官方文件🔗了解更多。

延伸閱讀