React Query - Lauviah0622/Lavi-Note GitHub Wiki

概念

自從前後端分離之後,什麼時候要打 API 拿資料就變成了非常複雜的問題。而這件事情進到 React 之後更加複雜了。React-query 將網頁中需要處理的狀態分為兩類:Client State 跟 Server State。Server State 包含了網頁需要向 Server 提取的資料以及資料的狀態,這些狀態有著非同步的特性。

React Query 這個 library 主要就是負責處理和 Server 相關的狀態的更新、操作相關的問題。

雖然這邊的編寫形式是以 API 為基礎,但是自己不太推薦這樣看,因為單一的情境會用到不只一種 API,而文件上有提供很多情境,而且也寫的更加詳細。

使用

  1. yarn add
  2. 在外層新增 Context Provider,並初始化 QueryClient 作為 Context 的 Value
  3. 使用 hooks

範例如下:

import { QueryClient, QueryClientProvider, useQuery } from "react-query";

const queryClient = new QueryClient();

function App() {
  const query = useQuery("queryKey", () => axios.get(url));

  return (
    <QueryClientProvider client={queryClient}>
      ...
      {query.isSuccess && query.data}
      ...
    </QueryClientProvider>
  );
}

useQuery

可想像成資料庫中的 SELECT (獲取資料),在使用上大部分也是對應到 request METHOD 中的 GET。使用的方式為

//api
const query = useQuery(queryKEy, queryFn, config?);

//e.g.

function todos() {
  const { isSuccess, data } = useQuery("todos", () => axios.get("/todos"));

  return <>{isSuccess && data}</>;
}

ANCHOR: 這邊加上 data 從 fresh => stale 那些過程

queryKey

queryKey 會作為內部查找 cache 的依據。整個 react-query 的 Context 中 cache 是共享的。換句話說

import { useQuery } from "react-query";

function foo() {
  const query = useQuery("todos", () => axios.get("/todos"));
}

function bar() {
  const query = useQuery("todos", () => axios.get("/todos"));
}

在設定得當的狀況下(指的是包含快取的過期時間等等的其他設定),如果在 foo 裡面已經發出 request 拿到資料,也成功拿到 response,因為已經儲存資料在 cache,所以 bar 並不會重新發出 request,而是直接用 cache 中的資料。

queryKey 可以是 string,或者是裡面含有可序列化內容的 array(序列化這個值很抽象,可以簡單理解成你的 array 要同時能夠呈現為 JSON 格式)。例如文件中的範例

useQuery('todos', ...)
useQuery(['todo', 5], ...)
useQuery(['todo', 5, { preview: true }], ...)

上面這幾種寫法都是合法的。但是要注意前面提到的一點

queryKey 會作為內部查找 cache 的依據

所以我們會把 request 中的參數放入 querykey。來讓 cache 能夠有合理的標籤做查找。像是:

useQuery(["todo", id], () => axios.get(`/todo/:${id}`));
useQuery(["todo", id, { preview }], () =>
  axios.get(`/todo/:${id}?preview=${preview}`)
);

那這種沒有帶參數的 queryKey,就適合放在列表形式的資料:

useQuery("todos", () => axios.get(`todos`));

另外一個需要注意的點是, queryKey 在經過內部處理後,object 中的順序還有 undefined 的值會被忽略。但是 array 的順序是重要的

// same
  useQuery(['todos', { status, page }], ...)
  useQuery(['todos', { page, status }], ...)
  useQuery(['todos', { page, status, other: undefined }], ...)

// different
  useQuery(['todos', 1, 'preview'], ...)
  useQuery(['todos', 'preview', 2], ...)

queryFn

React-query 的目標是自動化的處理 Server 方面的狀態,也就是自動處理拿資料的過程,然而實際怎麼樣去和 server 拿資料這點是可以自行選擇的。不管你是 graphQL,還是 RESTful ,又或者是用 axios 或者是 fetch API,只需要符合以下條件

  1. 回傳的值是 Promise
  2. 會依照成功或者失敗而進行 Promise 的 resolve 或者 reject

尤其是第二點,在使用 web API 的 fetch 時需要特別注意,fetch 只在這兩種情況下才會 throw Error(見 Fetch spec 的 Fetch API

  1. 無法發出 request
  2. response 本身格式有問題

而並不會在例如 404 等拿不的的狀況下 reject Promise。所以需要自行做包裝


TIPS:

有一個很方便的地方是,react-query 會自動把 queryKey 放入 queryFn 的參數。而參數形式為

{
  pageParam, queryKey;
}

所以稍微設計一下 queryFn 就可以不需要自己再輸入參數,像下面這樣

function Todos({ status, page }) {
  const result = useQuery(["todos", { status, page }], fetchTodoList);
}

// Access the key, status and page variables in your query function!
function fetchTodoList({ queryKey }) {
  const [_key, { status, page }] = queryKey;
  return new Promise();
}

config

這是整個 React-query 最精華之所在,在 config 可以設定各種參數來決定什麼樣的狀態要重新送出 request 拿取新的資料。但在設定 config 之前一定要了解的是 react-query 的預設值,下面做簡單總結:

  • 透過 useQuery 還有 useInfiniteQuery 拿回來的資料會馬上被視為 stalestale 狀態下,預設以下行為會重新 query 資料:
    • window refocus,可以透過 refetchOnWindowFocus 設定
    • 網路重新連線,可以透過 refetchOnReconnect 設定
    • Component 被 mount,可以透過 refetchOnMount 設定
    • 設定 refetch ,可以透過 refetchInterval 設定
  • 如果放在 cache 裡面的資料沒有被使用,會被視為 inactive,預設會在 5 分鐘後被回收,可以透過 cacheTime 來設定這個時間
  • 即使 query 失敗,也會自動嘗試重新 query 3 次,可以透過 retry 還有 retryDelay 來設定
  • Query 的資料即使不同 Component,只要是相同的 key 就會共享同一個資料的 reference。

Out of the box, React Query is configured with aggressive but sane defaults. Sometimes these defaults can catch new users off guard or make learning/debugging difficult if they are unknown by the user.

即使在文件裡面,都提到說這樣的預設值是比較「激進的」,透過 config 內部部份的參數,可以對 query 來作到更精細的設定。但是在設定之前,還是建議看一下 useQuery 的 API

下面會介紹一下自己用過的 config 參數:

select

很有可能一個 API 被不同的 component 呼叫,雖然 fetch 的是同樣的 API 資料,但是他們需要轉化成自己 component 需要的資料。例如有兩個 component:

function Todos() {
  const {data} = useQuery('todos', getTodos)

  const todos = data.map(todo => <li>{todo.title}</li>)
return (
  <ul>
    {todos}
  </ul>
)
}

function Stat() {
  const {data} = useQuery('todos', getTodos)
  const doneStat = data.filter(todo => todo.done).length;

  return <div>
  done: {doneStat}
  </div>;
}

兩個 component 都用到同一隻 API,但是資料使用的方式不同,這時候能夠使用 select。能夠對拿到的資料做轉化。

function Todos() {
  const {data} = useQuery('todos', getTodos)

  const todos = data.map(todo => <li>{todo.title}</li>)
return (
  <ul>
    {todos}
  </ul>
)
}

function Stat() {
  const {data: doneStat} = useQuery('todos', getTodos, {
    select: (data) => data.filter(todo => todo.done).length;
  })

  return <div>
  done: {doneStat}
  </div>;
}

keepValueExist

在預設情況下,如果 refetch 時會清空 query 中 data 的 reference。所以在 refetch 的時候可能會看到沒有資料的畫面。那當 keepValueExist 開啟後,在 API fetch 的時候就會維持之前的資料,等到 fetch 到資料時才更新為新的資料

變體

useInfiniteQuery

用在「載入更多」或者是無限滾動這類的 query。

useQueries

當你需要動態的進行 Query (例如依照某個 array 的內容),就需要使用 useQueries

useMutation

可以說對應到操作資料的部份,像是 POST, PATCH, PUT, DELETE 這些 method。使用方式為

const mutation = useMutation(mutationFn, config?)

mutation.mutate(variables, callbackConfigs)

//example
const mutation = useMutation(newTodo => axios.post('/todos', newTodo))

mutation.mutate({ id: new Date(), title: 'Do Laundry' })

其實自己覺得 useMutation 比起 useQuery 單純很多。

⚠️ **GitHub.com Fallback** ⚠️