react-query手把手教程①-入門react-query

修仙大橙子發表於2022-06-05

入門react-query

寫在前面

由於國內較少有比較系統的react-query教程,因此筆者結合官方文件以及官方課程的內容,希望寫一個較為全面的教程。本文將以各種例子作為切入點,儘可能通俗易懂地講解相關知識點。如果有錯誤,還請大家在評論區指出,筆者會盡快改正。

目錄

  • [入門react-query]() 已於2022-06-05更新

版本說明

本教程基於react-query@4版本編寫,此版本目前(2022-06-05)為alpha版本。

線上演示基於stackblitz 平臺

從在react中後端請求資料開始說起

在日常的開發中,免不了請求後端介面。在請求介面時,經常會涉及到以下處理

  • 載入狀態
  • 後端返回資料儲存
  • 如果介面有報錯資訊,展示報錯資訊
  • 重新整理資料
  • 等等

    下面來看一個滿足了上述處理的例子
     點我檢視例子①線上演示 

PS:以下程式碼將會從github請求一句富有禪意(逼格高)的話,並顯示在頁面上

例子①??

import * as React from 'react';

export default function App() {
  // 儲存 後端返回資料
  const [zen, setZen] = React.useState('');
  // 儲存 載入狀態
  const [isLoading, setIsLoading] = React.useState(false);
  // 儲存 是否請求成功
  const [isError, setIsError] = React.useState(false);
  // 儲存 後端返回的錯誤資料
  const [errorMessage, setErrorMessage] = React.useState('');

  const fetchData = () => {
    // 開始獲取資料,將isLoading置為true
    setIsLoading(true);

    fetch('https://api.github.com/zen')
      .then(async (response) => {
        // 如果請求返回status不為200 則丟擲後端錯誤
        if (response.status !== 200) {
          const { message } = await response.json();

          throw new Error(message);
        }

        return response.text();
      })
      .then((text: string) => {
        // 請求完成將isLoading置為false
        setIsLoading(false);
        // 介面請求成功,將isError置為false
        setIsError(false);
        // 儲存後端返回的資料
        setZen(text);
      })
      .catch((error) => {
        // 請求完成將isLoading置為false
        setIsLoading(false);
        // 介面請求錯誤,將isError置為true
        setIsError(true);
        // 儲存後端返回的錯誤資料
        setErrorMessage(error.message);
      });
  };

  React.useEffect(() => {
    // 初始化請求資料
    fetchData();
  }, []);

  return (
    <div>
      <h1>Zen from Github</h1>
      <p>{isLoading ? '載入中...' : isError ? errorMessage : zen}</p>
      {!isLoading && (
        <button onClick={fetchData}>{isError ? '重試' : '重新整理'}</button>
      )}
    </div>
  );
}

在上面的例子中

  • 使用isLoading來儲存載入狀態
  • 使用isError 來儲存介面是否有錯誤
  • 使用errorMessage 來儲存後端返回的報錯資訊
  • 使用zen來儲存 後端返回資料儲存 
  • 重新呼叫fetchData 方法來重新整理資料

該例子僅僅是請求一個介面,假如是一個真實的專案,鐵定不止這一個請求,因此我們將要寫大量重複程式碼,來滿足業務需求(內心os:其實是程式碼寫的太多影響效率,不能早下班(¬_¬),而且維護起來成本高)_

此時引入react-query可以減少與請求介面相關的程式碼,上面的例子使用react-query重寫如下:
 點我檢視例子②線上演示 
例子②??

import * as React from 'react';
import { useQuery } from 'react-query';

const fetchData = () => {
  return fetch('https://api.github.com/zen').then(async (response) => {
    // 如果請求返回status不為200 則丟擲後端錯誤
    if (response.status !== 200) {
      const { message } = await response.json();

      throw new Error(message);
    }

    return response.text();
  });
};

export default function App() {
  const zenQuery = useQuery(['zen'], fetchData); // ①

  return (
    <div>
      <h1>Zen from Github</h1>
      <p>
        {zenQuery.isLoading || zenQuery.isFetching
          ? '載入中...'
          : zenQuery.isError
          ? zenQuery.error?.message
          : data}
      </p>
      {!zenQuery.isLoading && !zenQuery.isFetching && (
        <button
          onClick={() => {
            zenQuery.refetch();
          }}
        >
          {zenQuery.isError ? '重試' : '重新整理'}
        </button>
      )}
    </div>
  );
}

對比一下,在引入了react-query之後,肉眼可見程式碼量在降低!!

不需要寫useState來管理因為請求介面帶來的額外狀態(如果使用react-redux、mobx等狀態管理庫,同樣會遇到類似的問題),同樣也不需要在useEffect(() => {}, [])中初始化呼叫介面,react-query會幫我們處理。

你可能對於上面例子中,在程式碼①引入useQuery鉤子時,傳入的引數不是很瞭解,接下來我們將會介紹這些傳入的引數。

查詢鍵(Query Keys)及查詢函式(Query Functions)

什麼是查詢鍵(Query Keys)及查詢函式(Query Functions)?

在大家日常開發的過程中,請求後端資料時:

  • 會先寫一個函式來請求後端介面的資料(如上面例子①中的fetchData函式)
  • 接著指定一個變數(如上面例子①中的zen)來儲存相關後端返回的資料,每個介面的變數針對不同的介面會起不同的名字,來標識不同的資料。

那麼在react-query中如何區分不同的介面獲取的不同資料呢?

回到例子②中,我們使用useQuery鉤子來獲取後端資料,程式碼如下:

const zenQuery = useQuery(['zen'], fetchData);
  • 其中['zen'] 就是react-query的查詢鍵,react-query通過不同的查詢鍵來標識(對映)不同介面(或是同一介面不同引數請求)返回的資料。在react-query@4中,查詢鍵必須是陣列。
  • fetchData就是我們請求後端介面的函式,也就是查詢函式。

PS:查詢鍵內的元素可以是巢狀陣列、物件、字串、數字

例如:['zen', { form: 'confucius' }]['zen', ['confucius', 'Lao Tzu']]

為了方便記憶,打個比方,你可以將查詢鍵看做是你儲存localStorage時的key,而value則是通過查詢函式查詢到資料後,將各種我們需要的狀態資料儲存進入value

PS:當然實際的處理過程及儲存的資訊會很複雜,不過思路基本上一致。

寫查詢鍵的一些小建議

解釋完查詢鍵和查詢函式後,筆者希望大家考慮一個問題,這個介面比較簡單,因此我們可以使用zen來作為查詢鍵,假如我有一個複雜的介面,此時應該如何更好的設計查詢鍵呢?

還是以github的介面為例,如果你想獲取到github中某個倉庫的issue列表,你可以這樣呼叫介面

https://api.github.com/repos/{owner}/{repo}/issues

具體一點,假如希望獲取react倉庫issue列表可以呼叫下面的介面,你可以在瀏覽器中開啟嘗試一下:

https://api.github.com/repos/facebook/react/issues

此時,你可以通過請求介面,拿到react倉庫內的issue列表。

以這個獲取倉庫issue列表介面為例,可以這樣寫查詢鍵
例子③??

['issues', owner, repo]

在這個查詢鍵中我們遵循了一個原則:從通用到特殊

首先我們獲取的資料型別是issue,我們需要在陣列的開頭放一個字串來標識資料型別,因此第一個引數我們設定為issues。在github中有許多倉庫,這些倉庫通常以使用者作為第一級標識,倉庫名是第二級標識,如下圖所示
react倉庫

因此第二個和第三個引數依次是ownerrepo

上面的例子中,我們沒有使用['issues', 'facebook', 'react']而是使用['issues', owner, repo]的原因是為了介紹在react-query中,使用變數作為查詢鍵的元素時,當變數的值變化後,react-query將會重新呼叫fetchData方法,獲取新的資料,並快取到對應變數值為key的快取中。

即發生下面的變化時,react-query將會重新呼叫fetchData方法,並將從後端獲取到的資料,快取在查詢鍵為['issues', 'vuejs', 'vue']對應的值中,同理我們在初始化呼叫介面時,獲取的資料時快取在查詢鍵為['issues', 'facebook', 'react']的對應值中:

['issues', 'facebook', 'react'] -> ['issues', 'vuejs', 'vue'] // 從查詢react倉庫的issue,變更為查詢vue倉庫的issue

下面的例子將會獲取react倉庫中最新一條issue,你可以檢視例子④的線上演示

將示例中輸入框內的:facebook更換為vuejs,將react更換為vue,點選【檢視最新issue資訊】按鈕,就可以看到vue倉庫最新的issue資訊(針對相關的資料快取,你可以想一下上面我們說過的例子)

點我檢視例子④線上演示

例子④??

import * as React from 'react';
import { useQuery } from 'react-query';

const fetchData = ({ queryKey }) => {
  const [, owner, repo] = queryKey;

  return fetch(`https://api.github.com/repos/${owner}/${repo}/issues`, {
    headers: {
      Authorization: '',
    },
  }).then(async (response) => {
    // 如果請求返回status不為200 則丟擲後端錯誤
    if (response.status !== 200) {
      const { message } = await response.json();

      throw new Error(message);
    }

    return response.json();
  });
};

export default function App() {
  const [inputOwner, setInputOwner] = React.useState('facebook');
  const [inputRepo, setInputRepo] = React.useState('react');
  const [queryKey, setQueryKey] = React.useState([inputOwner, inputRepo]);
  const issueQuery = useQuery(['issues', ...queryKey], fetchData);

  return (
    <div>
      <span>倉庫:</span>
      <input
        name={'owner'}
        value={inputOwner}
        onChange={(e) => setInputOwner(e.target.value)}
      />
      /
      <input
        name={'repo'}
        value={inputRepo}
        onChange={(e) => setInputRepo(e.target.value)}
      />
      <button
        onClick={() => {
          setQueryKey([inputOwner, inputRepo]);
        }}
      >
        檢視最新issue資訊
      </button>
      <div>
        <h1>
          倉庫{queryKey[0]}/{queryKey[1]}最新一條issue資訊
        </h1>
        <p>
          {issueQuery.isLoading
            ? '載入中...'
            : issueQuery.isError
            ? issueQuery.message
            : JSON.stringify(issueQuery.data[0])}
        </p>
      </div>
    </div>
  );
}

在這個例子中,當查詢鍵變數的值變化後,react-query將會自動請求變化後對應的資料,並且在查詢函式傳入的引數中,我們也可以拿到呼叫查詢函式時查詢鍵的值。

你可以在DevTool中,檢視react-query的快取資訊,幫助你理解:

使用DevTool

快取資訊