React Query 基礎使用指南

React-Query-logo

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 和配置可能需要更多的學習成本,例如使用 providesTagsinvalidatesTags 的邏輯可能需要一些時間來理解。
  • 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
image 49

所以如果要獲取 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

留言

目前沒有留言。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *