為什么要寫這篇文章
- 公司業(yè)務(wù)使用react-query,但目前只停留在“能用”的階段,離真正會(huì)用并且靈活使用API還有很長(zhǎng)距離
react-query介紹
react-query是React數(shù)據(jù)獲取(date-fetch)庫,在使用Hooks寫組件時(shí),發(fā)起異步請(qǐng)求時(shí),不僅需要管理請(qǐng)求狀態(tài),而且還需要處理異步數(shù)據(jù),為此要多寫幾個(gè)useState/useEffect來控制。
而react-query也是一個(gè)Hooks庫,使用很少的代碼完成對(duì)服務(wù)端的狀態(tài)管理,而且大多數(shù)情況下使用查詢useQuery和修改useMutation就可以了
我們知道redux可以輕松的管理客戶端狀態(tài),但并不適合處理異步和服務(wù)端狀態(tài),服務(wù)端狀態(tài)有以下比較復(fù)雜的點(diǎn):
- 緩存...(數(shù)據(jù)未變化時(shí)不去請(qǐng)求)
- 知道數(shù)據(jù)何時(shí)“過時(shí)”
- 在后臺(tái)更新“過時(shí)”的數(shù)據(jù)
- 分頁、延遲加載等性能優(yōu)化
- 結(jié)構(gòu)化共享并存儲(chǔ)查詢結(jié)果
而react-query正是為此而生,可以方便的管理服務(wù)端的狀態(tài)
安裝
# use npm
npm i react-query
# use yarn
yarn add react-query
使用
-
在App.tsx中創(chuàng)建全局實(shí)例
client,并通過QueryClientProvider將client傳遞下去,用于管理所有請(qǐng)求import './App.css' import { QueryClient, QueryClientProvider, } from 'react-query' import { ReactQueryDevtools } from 'react-query/devtools'; import Demo1 from './components/Demo1' // 創(chuàng)建一個(gè) client const queryClient = new QueryClient() function App() { return ( // 提供client <QueryClientProvider client={queryClient}> {/* 添加devtools */} {process.env.NODE_ENV === 'development' ? ( <ReactQueryDevtools initialIsOpen={false} position='bottom-right' /> ) : ( '' )} <Demo1 /> </QueryClientProvider> ) } export default App -
在組件中使用useQuery和useMutation,通過useQueryClient獲取到全局QueryClient實(shí)例,調(diào)用api管理react-query的請(qǐng)求,如
queryClient.invalidateQueries('posts')import axios from 'axios'; import { useMutation, useQuery, useQueryClient } from 'react-query'; type dataType = { id: string title: string } const Demo1 = () => { // 訪問App QueryClientProvider提供的client const queryClient = useQueryClient(); const query = useQuery('posts', () => axios.get('https://jsonplaceholder.typicode.com/posts')) console.log(query); const { data, isLoading, isError } = query; const { mutate } = useMutation(() => axios.delete('https://jsonplaceholder.typicode.com/posts/1'), { onSuccess: () => { // 錯(cuò)誤處理和刷新 queryClient.invalidateQueries('posts') }, }) if (isError) { return <div>error</div>; } if (isLoading) { return <div>loading</div>; } return ( <> <button onClick={() => { mutate() }} > Delete </button> <ul> {(data?.data as unknown as dataType[])?.map(d => <li key={d.id}>{d.title}</li>)} </ul> </> ) } export default Demo1useQuery-query.jpg
- useQuery接收一個(gè)唯一鍵和一個(gè)返回Promise的函數(shù)以及config
[queryKey, queryFn, config],如posts在內(nèi)部用于在整個(gè)程序中重新獲取數(shù)據(jù)、緩存和共享查詢等 - 通過打印query會(huì)看到,React-Query將所有的請(qǐng)求中間狀態(tài)進(jìn)行封裝
- isFetching 或者 status === 'fetching' 類似于isLoading,不過每次請(qǐng)求時(shí)都為true,所以使用isFetching作為loading態(tài)更好
- isLoading 或者 status === 'loading' 查詢沒有數(shù)據(jù),正在獲取結(jié)果中,只有“硬加載”時(shí)才為true,只要請(qǐng)求在cacheTime設(shè)定時(shí)間內(nèi),再次請(qǐng)求就會(huì)直接使用cache,即“isLoaindg = isFetching + no cached data”
- isError 或者 status === 'error' 查詢遇到一個(gè)錯(cuò)誤,此時(shí)可以通過 error 獲取到錯(cuò)誤
- isSuccess 或者 status === 'success' 查詢成功,并且數(shù)據(jù)可用,通過 data 獲取數(shù)據(jù)
- isIdle 或者 status === 'idle' 查詢處于禁用狀態(tài)
示例:
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from 'react-query'
import { getTodos, postTodo } from '../my-api'
// 創(chuàng)建一個(gè) client
const queryClient = new QueryClient()
function App() {
return (
// 提供 client 至 App
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
function Todos() {
// 訪問 client
const queryClient = useQueryClient()
// 查詢
const query = useQuery('todos', getTodos)
// 修改
const mutation = useMutation(postTodo, {
onSuccess: () => {
// 錯(cuò)誤處理和刷新
queryClient.invalidateQueries('todos')
},
})
return (
<div>
<ul>
{query.data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<button
onClick={() => {
mutation.mutate({
id: Date.now(),
title: 'Do Laundry',
})
}}
>
Add Todo
</button>
</div>
)
}
render(<App />, document.getElementById('root'))
重要知識(shí)點(diǎn)
-
refetchOnWindowFocus
const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, }, }, })refetchOnWindowFocus默認(rèn)為true,用戶短暫離開再返回應(yīng)用頁時(shí),數(shù)據(jù)就會(huì)被標(biāo)記為過時(shí),這時(shí)react-query會(huì)在后臺(tái)自動(dòng)請(qǐng)求新的數(shù)據(jù),通過設(shè)置refetchOnWindowFocus為false禁用
-
query-keys
-
字符串作為query-keys時(shí),會(huì)在內(nèi)部轉(zhuǎn)換為數(shù)組
useQuery('posts', ...) // queryKey === ['posts'] -
數(shù)組作為queryKey,查詢功能依賴于變量,類似于useEffect,則將其包含在查詢鍵值中,盡量使用數(shù)組
useQuery(["posts", postId], ...);
-
-
query-functions
任何一個(gè)返回Promise的函數(shù)
useQuery(["todos"], fetchAllTodos); useQuery(["todos", todoId], () => fetchTodoById(todoId)); useQuery(["todos", todoId], async () => { const data = await fetchTodoById(todoId); return data; }); // 通過解構(gòu)queryKey可以拿到傳遞的query-keys useQuery(["todos", todoId], ({ queryKey }) => fetchTodoById(queryKey[1]), { enabled: false, retry: 3, select: data => {}, onSuccess: data => {}, onError: error => {} ... });useQuery的config配置有很多,API
-
并行查詢parallel-queries
同時(shí)執(zhí)行的查詢
function App () { // 下面的查詢將自動(dòng)地并行執(zhí)行 const usersQuery = useQuery('users', fetchUsers) const teamsQuery = useQuery('teams', fetchTeams) const projectsQuery = useQuery('projects', fetchProjects) ... }React-query提供useQueries動(dòng)態(tài)并行查詢
function App({ users }) { const userQueries = useQueries( users.map((user) => { return { queryKey: ["user", user.id], queryFn: () => fetchUserById(user.id), }; }), ); } -
有依賴的查詢 enabled
具有相依性的query:當(dāng)有多個(gè)query 設(shè)定相依性后,前一個(gè)query 必須成功執(zhí)行并取得資料,下一個(gè)query 才會(huì)接續(xù)執(zhí)行。
開啟/關(guān)閉查詢:假設(shè)我們有一個(gè)定時(shí)查詢,通過refetchInterval來實(shí)現(xiàn),但是當(dāng)一個(gè)彈窗打開的時(shí)候我們可以暫停這個(gè)查詢,避免彈窗后面的內(nèi)容發(fā)生變更。
demo -
緩存
-
useQuery和useInfiniteQuery生成的查詢實(shí)例會(huì)立即將緩存數(shù)據(jù)視為過時(shí)(slate)的
useQuery-slate.jpg
-
- staleTime(不新鮮時(shí)間) 默認(rèn)0,可全局或單獨(dú)配置,在此段時(shí)間內(nèi)再次遇到相同key的請(qǐng)求,不會(huì)再去獲取數(shù)據(jù),直接從緩存中獲取,isFetching也為false,如果設(shè)置為Infinity,則當(dāng)前查詢的數(shù)據(jù)只會(huì)獲取一次,在整個(gè)網(wǎng)頁的生命周期內(nèi)緩存
useQuery('posts', axios => ('https://jsonplaceholder.typicode.com/posts'), {
select: ({ data }) => {
return data.data;
},
cacheTime: Infinity,
staleTime: Infinity,
});
通過devTools可以看到此時(shí)數(shù)據(jù)是fresh狀態(tài)

這時(shí)候頁面上的所有相同query-keys的請(qǐng)求都會(huì)被緩存起來,想要重新請(qǐng)求就需要清空緩存
queryClient.invalidateQueries('todos');
-
cacheTime(緩存時(shí)間) 數(shù)據(jù)在內(nèi)存中的緩存時(shí)間,默認(rèn)5分鐘,在不設(shè)置slateTime時(shí),如果緩存期內(nèi)遇到相同key的請(qǐng)求,雖然會(huì)直接使用緩存數(shù)據(jù)呈現(xiàn)UI,但還是會(huì)獲取新數(shù)據(jù),待獲取完畢后切換為新數(shù)據(jù),isFetching為true;如果某個(gè)queryKey未被使用時(shí),這個(gè)query就會(huì)進(jìn)入inactive狀態(tài),如果在cacheTime設(shè)定的時(shí)間內(nèi)未被使用的話,這個(gè)query及其data就會(huì)被清除
useQuery-inactive.jpg
可以看到此時(shí)["post", 3]和["post", 2]是inactive狀態(tài),過設(shè)定的cacheTime后會(huì)被清除


