簡單介紹非同步的狀態管理工具 react query。
上一篇的最後使用 Zustand 來取得 github 的 api 資料,在非同步的處理裡有另外一個非常好用的非同步狀態管理工具 reqct query,今天簡單講講如何使用 react-query 來做到資料的管理。
在開始使用 react-query 之前先使用 react router 來建立一個簡單的路徑,分別是 react、vue、angular,並且在 Home 簡單建立一個切換的清單來切換頁面。
import {
createBrowserRouter,
RouterProvider,
Outlet,
Link,
} from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: <Home />,
children: [
{
path: "/react",
element: <Page name="React" />,
},
{
path: "/vue",
element: <Page name="Vue" />,
},
{
path: "/angular",
element: <Page name="Angular" />,
},
],
},
]);
function App() {
return <RouterProvider router={router}></RouterProvider>;
}
const path = [
{ path: "/react", name: "react" },
{ path: "/vue", name: "vue" },
{ path: "/angular", name: "angular" },
];
function Home() {
return (
<div>
<h1>react query sample</h1>
<div style={{ display: "flex", gap: "20px" }}>
{path.map(({ path, name }) => (
<Link key={path} to={path}>
{name}
</Link>
))}
</div>
<Outlet />
</div>
);
}
type Props = {
name: string;
};
function Page({ name }: Props) {
return (
<div>
<h1>{name}</h1>
</div>
);
}
這樣就完成初步的準備了。
接著就輪到 react-query 的時候了。
npm install @tanstack/react-query
oryarn add @tanstack/react-query
安裝完成之後就可以從 @tanstack/react-query
引入useQuery, QueryClient, QueryClientProvider
這三個東西,並且透過 new QueryClient()
來建立一個 instance,然後把建立出來的 instance 透過 QueryClientProvider
元件把我們的 router 包起來。
import { useQuery, QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router}></RouterProvider>
</QueryClientProvider>
);
}
到這邊我們使用了 QueryClient
跟 QueryClientProvider
來做基礎的設定,接下來要介紹在 react-query 裡面最常使用的 useQuery
了。
useQuery 是一個 hook,它接收一個物件參數,return 一個物件,裡面包含了狀態跟資料等內容。
const queryObj = useQuery({queryKey:[’key’], queryFn:yourFunction})
除了 queryKey
跟 queryKey
是必要 value 以外其他的設定都有預設值,後面會再提到。
queryKey
: 一個 array 裡面可以包含任意 value,useQuery 會把 query key 做處理,產出一組獨一無二的 id 來作為辨識,每個 query key 都有相對應的 query,不應該有相同的 query key 執行不同的 query。
queryFn
: 一個 function 回傳一個 Promise 物件,必須 resolve 回傳 data 或是 throw error,data 不能是 undefined。
先準備一個會回傳 promise 的 function。
function getNumber(): Promise<number> {
return new Promise((res) => {
setTimeout(() => {
res(Math.random() * 100);
}, 3000);
});
}
這邊在 Home 裡面使用 useQuery 看看。
function Home() {
const { data, status } = useQuery({
queryKey: ["getNumber"],
queryFn: getNumber,
});
return (
<div>
<h1>react query sample</h1>
<p>status: {status}</p>
<p>data: {data}</p>
<div style={{ display: "flex", gap: "20px" }}>
{path.map(({ path, name }) => (
<Link key={path} to={path}>
{name}
</Link>
))}
</div>
<Outlet />
</div>
);
}
這邊從 useQuery 回傳的物件裡面取出 data 跟 status 放在畫面上看一下。
重整畫面會看到 status 先出現 loading,然後當 promise 執行結束,status 變成 success 時 data 就會出現數字了。
這邊我開啟開發者工具點擊了開發者工具再重新點擊視窗,經過 3 秒後會發現數字改變了,代表有重新執行 useQuery 但是 status 並沒有出現 loading。
這是因為 useQuery 的設定裡面有一個 refetchOnWindowFocus
的參數預設為 true,當用戶重新 focus 畫面時會再次執行 query 確保畫面是最新的狀態。
如果希望讓用戶知道現在是否正在更新的話可透過回傳的參數裡面的 isFetching
來判斷是否正在重新取得資料。
另外沒有出現 loading 的原因是 react-query 每次都會將你上次執行 query 時的結果保留下來,當在結果成功時主動幫你更新畫面,所以才不會出現 loading 的 status。
稍微修改一下 getNumber 的內容,讓 getNumber 只會成功第一次。
let isFirst = true;
function getNumber(): Promise<number> {
console.log("isFirst", isFirst);
return new Promise((res, rej) => {
!isFirst && rej("nono");
setTimeout(() => {
res(Math.random() * 100);
isFirst = false;
}, 3000);
});
}
再看看 useQuery 會如何處理。
如果遇到 query 失敗的情形 react query 並不會馬上把錯誤訊息拋出,而是會自動重新執行 query function,預設是重新執行 3 次,並且預設再每次失敗時會多延後一秒執行。
e.g., 第一次失敗 1 秒後重新執行,第二次失敗 2 秒後重新執行…
接著來使用一開始建立的 react、vue、angular 的路徑吧。
先建立一個 getData 的 function,依照傳入的 query 搜尋 github 上面的 repo,並回傳整理過的資料。
async function getData(query: string): Promise<Data[]> {
const res = await fetch(
`https://api.github.com/search/repositories?q=${query}`
);
const data = await res.json();
return data.items;
}
接著在 page 裡面使用 useQuery 來呼叫 getData。
function Page({ name }: Props) {
const { isSuccess, isLoading, isError, data } = useQuery({
queryKey: [name, name], // 傳入的 queryFn 的參數也必須放在 queryKey 裡面
queryFn: () => getData(name), // 需要在 queryFn 傳入參數的話可以這樣寫
});
return (
<div>
<h1>{name}</h1>
{isLoading && <p>Loading...</p>}
{isError && <p>Error...</p>}
{isSuccess && (
<ul>
{data.map((item) => (
<li key={item.id}>
<h2>{item.full_name}</h2>
<p>
Repo Url:
<a
href={item.html_url}
target="_blank"
rel="noreferrer noopener"
>
{item.html_url}
</a>
</p>
</li>
))}
</ul>
)}
</div>
);
}
當 queryFn 裡面有使用參數時,必須要稍微調整寫法把參數傳進去,另外也要在 queryKey 裡面加上你寫的參數,確保每一個 query 都是獨一無二的。
透過 useQuery 就可以做到用戶訪問過的頁面不會再出現 Loading 的畫面,而是先將上一次 query 的結果先顯示給用戶,然後在背景執行 query,然後再更新在畫面上。
另外當 useQuery 的 query key 相同時即使是沒有訪問過的頁面也可以先顯示在畫面上。
const router = createBrowserRouter([
{
path: "/",
element: <Home />,
children: [
{
path: "/react",
element: <Page name="React" />,
},
{
path: "/react2",
element: <Page name="React" />,
},
{
path: "/vue",
element: <Page name="Vue" />,
},
{
path: "/angular",
element: <Page name="Angular" />,
},
],
},
]);
const path = [
{ path: "/react", name: "react" },
{ path: "/react2", name: "react2" },
{ path: "/vue", name: "vue" },
{ path: "/angular", name: "angular" },
];
新增一個 react2 的路徑但是傳入的 name 相同所以 queryKey 跟會跟 react 相同。
可以看到即使是第一次訪問 react2 的頁面,但是因為 query key 相同,所以可以取得在 react 頁面已經取得的資料結果。
下一篇簡單介紹 react hook form
如果內容有誤再麻煩大家指教,我會馬上修改。
這個系列的文章會同步更新在我個人的 Medium,歡迎大家來看看 👋👋👋
Medium