最近重構了幾個專案,發現一個有趣的現象:我們團隊寫的 Redux 程式碼裡,大概有七成都在處理 API 相關的邏輯。這讓我開始思考,是不是有更好的方式來管理這些資料?
後來接觸到 TanStack Query,才發現原來問題出在我們把所有東西都當成「狀態」來處理。其實仔細想想,從伺服器來的資料和本地的 UI 狀態,本質上就是完全不同的東西。
為什麼要區分伺服器狀態和本地狀態?
先說說什麼是伺服器狀態吧。簡單來說,就是那些存在後端資料庫、需要透過 API 取得的資料。這類資料有幾個特點讓它們很難搞:
你永遠不知道什麼時候會被其他人改掉
每次要用都得發請求,還要處理載入和錯誤狀態
要考慮快取,不然用戶體驗會很差
網路斷了要重試,資料過期要更新
而本地狀態就單純多了,像是 modal 開關、表單輸入、使用者選的主題顏色這些,完全在前端控制,不需要跟後端打交道。
我以前的做法是把這兩種狀態都塞進 Redux 裡管理,結果寫了一堆 action 和 reducer 來處理 API 請求。現在回頭看,真的是自找麻煩。
在 React 專案裡怎麼分工?
經過這幾年的實戰,我現在的做法是這樣:
簡單的元件狀態就用 useState,不要想太多。比如一個下拉選單的開關狀態,用 useState 就夠了,搞個 Redux 反而是過度設計。
需要跨元件共享的狀態才考慮 Redux 或 Zustand。說實話,現在我更偏好 Zustand,因為它的 API 簡單多了,不像 Redux 要寫一堆 boilerplate。不過如果專案已經用 Redux 了,而且大家都熟悉,那就繼續用也沒問題。
所有 API 相關的資料交給 TanStack Query。這真的是改變遊戲規則的工具,它幫你處理了所有麻煩事:快取、重試、背景更新、loading 狀態...你能想到的都有。
Vue 專案也是一樣的道理
Vue 生態系統其實也面臨同樣的問題。我有朋友的 Vue 專案,Vuex 裡面一大堆處理 API 的程式碼,看得頭都大了。
現在 Vue 3 有了 Composition API,處理本地狀態變得更簡單。全域狀態的話,Pinia 是新的推薦選擇(比 Vuex 好用太多了)。而伺服器狀態?一樣用 TanStack Query 的 Vue 版本。
有意思的是,很多人擔心用了 TanStack Query 會不會和 Pinia 衝突。其實完全不會,它們各司其職:Pinia 管理你的業務邏輯和 UI 狀態,TanStack Query 專門處理 API 資料。
TanStack Query 到底好在哪?
程式碼少得驚人
我來給你看個對比。以前用 Redux 處理一個簡單的 todo list API:
// 要寫 action
export const fetchTodos = () => async (dispatch) => {
dispatch({ type: 'FETCH_TODOS_REQUEST' })
try {
const response = await fetch('/api/todos')
const data = await response.json()
dispatch({ type: 'FETCH_TODOS_SUCCESS', payload: data })
} catch (error) {
dispatch({ type: 'FETCH_TODOS_FAILURE', error })
}
}
// 還要寫 reducer
const todosReducer = (state = initialState, action) => {
switch (action.type) {
case 'FETCH_TODOS_REQUEST':
return { ...state, loading: true }
case 'FETCH_TODOS_SUCCESS':
return { ...state, loading: false, data: action.payload }
case 'FETCH_TODOS_FAILURE':
return { ...state, loading: false, error: action.error }
default:
return state
}
}
// 使用的時候還要 dispatch
useEffect(() => {
dispatch(fetchTodos())
}, [])現在用 TanStack Query:
const { data, isPending, error } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
})就這樣,沒了。而且功能還更強大,自動幫你處理重試、快取、背景更新...
快取機制超聰明
TanStack Query 的快取真的很聰明。你設定好 staleTime 和 cacheTime,它就知道什麼時候該重新請求,什麼時候可以直接用快取。
更酷的是,如果多個元件同時請求相同的資料,它會自動合併成一個請求。使用者切換頁面再回來,資料還在,但如果太久沒用,它會在背景悄悄更新。這些如果要自己實現,程式碼量可不少。
樂觀更新讓體驗更好
你知道什麼最影響用戶體驗嗎?就是按下按鈕後要等個一兩秒才看到結果。TanStack Query 的樂觀更新可以讓 UI 立即響應,然後在背景處理真正的請求。如果請求失敗了,它會自動回滾,用戶甚至不會察覺。
我在做一個即時協作的專案時,這個功能救了我一命。用戶的每個操作都能立即看到效果,體驗好太多了。
什麼時候用什麼工具?
經過這段時間的使用,我總結了一些經驗:
需要 TanStack Query 的場景:
專案大量依賴後端 API(廢話,現在哪個不是)
需要實現無限滾動、分頁這類功能
多個元件要共享同一份伺服器資料
對用戶體驗要求高,需要樂觀更新
還是需要 Redux/Pinia 的場景:
複雜的多步驟表單(每一步的資料要暫存)
全域的 UI 狀態(通知系統、主題設定)
需要 time-travel 除錯功能
團隊已經很熟悉 Redux,而且用得好好的
說實話,大部分專案兩個都需要。我現在的標準配置是:本地狀態用框架自帶的(useState、Composition API),業務邏輯用 Redux/Pinia,API 資料用 TanStack Query。
從 Redux 遷移的一些建議
如果你正在考慮遷移,我的建議是不要一次全換。可以先在新功能上試用 TanStack Query,感受一下差異。等團隊熟悉了,再逐步遷移舊程式碼。
遷移過程中你會發現,原本一大堆的 Redux 程式碼可以刪掉了。我們有個專案,遷移後程式碼量減少了將近 40%,而且功能還更強大了。
效能優化的小細節
TanStack Query 在效能優化上做了很多工作:
請求去重是我最喜歡的功能之一。假設你有三個元件同時 mount,都需要用戶資料,TanStack Query 只會發一個請求,然後把結果分享給三個元件。
結構共享也很巧妙。當資料更新時,沒變的部分會保持相同的引用,這樣可以避免不必要的重新渲染。配合 React.memo 使用效果很好。
預取功能可以讓你在用戶點擊前就載入資料。比如用戶 hover 在連結上時就開始載入,點進去就能立即看到內容。
一些實際使用的坑
當然,沒有工具是完美的。使用 TanStack Query 也踩過一些坑:
快取 key 的設計要想清楚。如果設計不好,會導致快取失效或者資料不同步。
不要什麼都快取。有些資料真的不需要快取,比如即時股價、線上人數這種。
錯誤處理要做好。雖然 TanStack Query 提供了錯誤重試,但有些錯誤(比如 401)不應該重試。
總結
TanStack Query 不是銀彈,它不能解決所有狀態管理的問題。但在處理伺服器狀態這件事上,它確實是目前最好的選擇之一。
最重要的是理解不同類型的狀態需要不同的處理方式。不要試圖用一個工具解決所有問題,那樣只會讓架構變得複雜難維護。
如果你還在用 Redux 處理所有的 API 請求,真的建議試試 TanStack Query。相信我,一旦用過就回不去了。