React Query 是一個 data-fetching 庫,幫助你更有效的管理 React 中的非同步狀態。
Redux-Toolkit Query vs React Query
使用過 RTK Query 和 React Query 後,我個人是更喜歡 React query,畢竟好上手、開發效率高,而且需要的功能基本都已經提供不需要自己再額外去處理,所以我認為學習 React Query 是划算的”投資”。
下面簡單對比一下 RTK Query 和 React Query 的優缺點,可以根據自己的專案需求來選擇要使用哪一個。
Redux-Toolkit Query
優點:
- Redux生態系:Redux 生態系的一部分,與 Redux Tookit 集成,無需再去使用別的狀態管理庫。如果有過使用 Redux 的經驗能很快上手。
缺點:
- 狀態管理庫的選擇: 狀態管理庫只能使用 RTK,不夠靈活、彈性。
- API 和配置的複雜性:RTK Query 的部分 API 和配置可能需要更多的學習成本,例如使用
providesTags
和invalidatesTags
的邏輯可能需要一些時間來理解。 - Pagination 功能: RTK Query 本身不提供 Pagination 功能,需要在 endpoint 中傳入
page
參數,相對而言不如 React Query 自帶的useInfiniteQuery
那樣方便。
React Query
優點:
- 狀態管理庫的選擇: 比較靈活、彈性,可以和任何狀態管理庫集成。
- 學習成本低、開發效率高:提供開箱即用的 hook,可以隨時在組件中使用。
- Pagination 功能:自帶
useInfiniteQuery
hook,能更方便的處理需要分頁請求的資料。
缺點:
- 狀態管理:需要搭配 Context 或者其他狀態管理庫來管理狀態。
- 與 Redux 整合:如果專案中已使用 Redux,與 React Query 整合可能需要額外的工作。
小結
總結一下,React Query 適合中小型需要快速開發和管理較為簡單的狀態的專案。而 Redux Toolkit Query 適合需要管理更複雜的狀態或已經使用 Redux 的專案。
以上這些只是比較基本的比較,如果需要了解更詳細的比較,可以看官方寫的 Comparison
安裝 React Query
npm i react-query
# or
yarn add react-query
# or
pnpm add @tanstack/react-query
與瀏覽器的相容性:
- Chrome >= 91
- Firefox >= 90
- Edge >= 91
- Safari >= 15
- iOS >= 15
- Opera >= 77
建議可以使用 eslint 插件來協助開發:
npm i -D @tanstack/eslint-plugin-query
# or
pnpm add -D @tanstack/eslint-plugin-query
# o
yarn add -D @tanstack/eslint-plugin-query
相關配置請看:ESLint plugin query
創建 Client
首先需要使用 QueryClient
創建一個 client,並在 App 組件外包裹 <QueryClientProvider>
,將 client 提供給其他組件做使用:
import React from 'react'
import ReactDOM from 'react-dom/client'
import { QueryClientProvider, QueryClient, QueryCache } from '@tanstack/react-query'
import App from './App.jsx'
const queryClient = new QueryClient()
ReactDOM.createRoot(document.getElementById('root')).render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
)
使用 Hook 發送 CRUD 請求
- GET 請求使用
useQuery
queryFn
為請求的函數- POST, PUT, PATCH, DELETE 使用
useMutation
mutationFn
為請求的函數
import React from 'react'
import { useQueryClient, useQuery, useMutation } from '@tanstack/react-query'
import { getStudents, updateStudent, postStudent, deleteStudent } from './api'
export const App = () => {
const students = useQuery({
queryKey: ['students'],
queryFn: getStudents
})
const addStudent = useMutation({
mutationFn: postStudent,
})
const editStudent = useMutation({
mutationFn: updateStudent,
})
const delStudent = useMutation({
mutationFn: deleteStudent,
})
...
}
useQuery
useQuery 的參數是一個物件,常見的屬性有:
queryKey: unknown[]
: 必填。是用來識別請求的唯一鍵,由可序列化的值組成的陣列。- 當此鍵值變更時,query 將自動更新(enabled 非 false 時)。
- 陣列中通常包括請求的名稱(API endpoint名稱)、請求所需的參數(ID, page…等)
queryFn: (context: QueryFunctionContext) => Promise<TData>
: 必填。傳回一個將解析資料或拋出錯誤的 promise。資料不能是 undefined
const { data: students, isSuccess, isPending } = useQuery({
queryKey: ['students'],
queryFn: getStudents,
})
回傳的主要內容包括:
- data
- error
- isSuccess, isLoading, isError, isPending…等請求狀態,以及 status, fetchStatus
data 是一個物件,其中又會包含請求的相關內容:
- data: 這才是真正的 Response
- config
- headers
- request
- status
所以如果要獲取 Response,應該要取兩層 data,這是一開始比較容易出錯的地方。
queryKey
React Query 需要藉由 queryKey 來區分多個不同的查詢請求,以及對請求的結果進行快取。
當 queryKey 發生變化時,React Query 將會自動重新發送請求。
假設我需要使用名稱、性別來對數據進行篩選,就可以將篩選的內容作為元素放進 queryKey:
const [filters, setFilters] = useState({ name: undefined, gender: undefined })
const { data: students, isSuccess, isPending } = useQuery({
queryKey: ['students', filters],
queryFn: ({ signal }) => getStudents(filters),
})
當 name 或者 gender 改變時就會自動重新發送請求獲取相應的數據。
enable
如果要禁止 query 自動執行,可以設置 enabled: false
。
舉個簡單的例子,如果沒有 id 的時候就不請求 getStudentById,可以寫成:
const { data: student, isSuccess } = useQuery({
queryKey: ['students', id],
queryFn: () => getStudentById(id),
enabled: !!id,
})
placeholderData & initialData
通常我們會希望在沒有拿到數據之前給定一個初始值,比如說空陣列。這時候就可以傳入 placeholderData
或者 initialData
不過區別在於,initialData 會被存到快取中,而 paceholderData 不會,通常來說 placeholderData 會比較符合我們常見的需求。
const { data: students, isSuccess, isPending } = useQuery({
queryKey: ['students', filters],
queryFn: ({ signal }) => getStudents(filters),
placeholderData: [],
})
useMutation
useMutation 的參數是一個物件,常見的屬性有:
mutationFn: (variables: TVariables) => Promise<TData>
: 必填。執行非同步任務並回傳 promise 函數。onSuccess: (data: TData, variables: TVariables, context?: TContext) => Promise<unknown> | unknown
: 當 mution 成功時觸發該函數,並將傳遞 mutation 的結果。
import React, { useState } from 'react'
import { useQueryClient, useQuery, useMutation } from '@tanstack/react-query'
import { getStudents, updateStudent, postStudent, deleteStudent } from './services/api'
const App = () => {
const queryClient = useQueryClient()
const addStudent = useMutation({
mutationFn: postStudent,
onSuccess: () => {
},
})
const editStudent = useMutation({
mutationFn: updateStudent,
onSuccess: (data, variables, context) => {
},
})
const delStudent = useMutation({
mutationFn: deleteStudent,
onSuccess: () => {
},
})
...
}
mutation 的調用方式有兩種:
- 同步:
addStudent.mutate()
- 非同步:
addStudent.mutateAsync()
返回的是 promise
傳入 mutate 的參數會帶入到 useMutation 的 mutateFn 中。
/* mutate */
addStudent.mutate(
data,
{
onSuccess: () => {
console.log('Success')
},
onError: (err) => {
console.log(err)
}
}
)
/* mutateAsync */
try {
await addStudent.mutateAsync(data)
console.log('Success')
} catch (err) {
console.log(err)
}
// or
addStudent.mutateAsync(data)
.then(() => console.log('Success'))
.catch(() => console.log(err))
要注意的是 useMutation 和 mutate 都可以傳入 callback,執行順序上 useMutation 的 callback 會先於 mutate 的 callback。
invalidateQueries
一般來說在新增、編輯、刪除資料後,會需要重新查詢資料,使用 React query 的話不需要自行再調用函數重新發送請求,只需要調用 queryClient.invalidateQueries()
。
invalidateQueries 的目的是使 query 無效,當 query 一無效就會重新執行查詢。
invalidateQueries 的參數是一個物件。如果需要使特定變數的查詢失效,可以傳入 queryKey
,和 useQuery 中的 queryKey
是一樣的。
const { data: students, isSuccess, isPending } = useQuery({
queryKey: ['students', filters],
queryFn: ({ signal }) => getStudents(filters, signal),
placeholderData: [],
})
const addStudent = useMutation({
mutationFn: postStudent,
onSuccess: () => {
return queryClient.invalidateQueries({ queryKey: ['students'] })
},
})
const editStudent = useMutation({
mutationFn: updateStudent,
onSuccess: (data, variables, context) => {
return queryClient.invalidateQueries({ queryKey: ['students', variables.id] })
},
})
如果希望 invalidateQueries 完成後再結束 mutation,記得要 return