入門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中有許多倉庫,這些倉庫通常以使用者作為第一級標識,倉庫名是第二級標識,如下圖所示
因此第二個和第三個引數依次是owner
和repo
。
上面的例子中,我們沒有使用['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的快取資訊,幫助你理解: