概述
React Query 時一個異步狀態(tài)管理庫,核心的概念有三個:
- 查詢(Queries)
- 修改(Mutations)
- 查詢錯誤處理(Query Invalidation)
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
import { getTodos, postTodo } from "../my-api";
// 創(chuàng)建一個 client
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
);
}
function Todos() {
// 訪問 client
const queryClient = useQueryClient();
// 查詢
const query = useQuery(["todos"], getTodos);
// 修改
const mutation = useMutation(postTodo, {
onSuccess: () => {
// 錯誤處理和刷新
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"));
默認(rèn)配置
- 默認(rèn)情況下,通過useQuery或useInfiniteQuery生成的查詢實例會將緩存的數(shù)據(jù)視為過時(stale)的。
- 失敗的查詢將靜默重試 3 次,在捕獲并向 UI 顯示錯誤之前,會有指數(shù)級的后退延遲(exponential backoff delay)。
所謂指數(shù)級的后退延遲,是指每次重試時,會指數(shù)級增加等待時間,也即是指數(shù)級的降低訪問頻率。
要更改這一點,你可以將查詢的默認(rèn)
retry和retryDelay選項更改為3之外的其他值或指數(shù)后退函數(shù)。
- 默認(rèn)情況下,查詢結(jié)果在結(jié)構(gòu)上是共享的,以檢測數(shù)據(jù)是否確實發(fā)生了更改。如果沒有,則數(shù)據(jù)的引用保持不變。
查詢 Queries
FetchStatus
在任何給定時刻,查詢只能處于以下狀態(tài)之一:
-
isLoading或者status === 'loading'- 查詢暫時還沒有數(shù)據(jù) -
isError或者status === 'error'- 查詢遇到一個錯誤 -
isSuccess或者status === 'success'- 查詢成功,并且數(shù)據(jù)可用
除了status字段,result對象,還會有一個額外的fetchStatus屬性,它有以下選項:
-
fetchStatus === 'fetching'- 正在查詢中 -
fetchStatus === 'paused'- 查詢想要獲取,但它被暫停了。在網(wǎng)絡(luò)模式中閱讀更多相關(guān)信息 -
fetchStatus === 'idle'- 該查詢處于閑置狀態(tài)
為什么有兩種表示狀態(tài)的東西(status/fetchStatus)?
后臺刷新和數(shù)據(jù)過期重試(stale-while-revalidate)的邏輯使status和fetchStatus的所有組合成為了可能。比如說:
- 一個
state='success'的查詢通常處于fetchStatus='idle'狀態(tài)。但如果同時有后臺重新獲取動作,它也可能為fetchStatus='fetching'狀態(tài)。 - 一個沒有數(shù)據(jù)的查詢通常處于
status='loading'狀態(tài)和fetchStatus='loading狀態(tài)。如果同時無網(wǎng)絡(luò)連接,它也可能為fetchStatus='paused'狀態(tài)。
所以請記住,一個查詢可以處于fetchStatus='loading'狀態(tài),但沒有實際的在獲取數(shù)據(jù)。 如何理清兩者關(guān)系?這里有一個簡單的經(jīng)驗法則:
-
status告訴我們有關(guān)data的狀態(tài):有或者沒有? -
fetchStatus告訴我們有關(guān)queryFn的狀態(tài):在執(zhí)行還是沒在執(zhí)行?
有效的使用 React Query Keys
https://tkdodo.eu/blog/effective-react-query-keys
查詢鍵值是 hash 決定的!
這意味著,不管對象中鍵值的順序如何,以下所有查詢都被認(rèn)為是相等的:
useQuery(['todos', { status, page }], ...);
useQuery(['todos', { page, status }], ...);
useQuery(['todos', { page, status, other: undefined }], ...);
但是,以下查詢鍵值不相等。這些數(shù)組項的順序很重要,因為它們的散列信息并不相同!
useQuery(['todos', status, page], ...);
useQuery(['todos', page, status], ...);
useQuery(['todos', undefined, page, status], ...);
useQuery 和 useInfiniteQuery 共享緩存,不能使用同樣的 key。因為兩種數(shù)據(jù)請求結(jié)構(gòu)不同。
useQuery(['todos'], fetchTodos)
// ?? this won't work
useInfiniteQuery(['todos'], fetchInfiniteTodos)
// ? choose something else instead
useInfiniteQuery(['infiniteTodos'], fetchInfiniteTodos)
如果查詢依賴變量,把他們放在 Query Key 中
function Todos({ todoId }) {
const result = useQuery({
queryKey: ['todos', todoId],
queryFn: () => fetchTodoById(todoId),
})
}
自動重取數(shù)據(jù)
function Component() {
const [filters, setFilters] = React.useState()
const { data } = useQuery(['todos', filters], () => fetchTodos(filters))
// ? set local state and let it "drive" the query
return <Filters onApply={setFilters} />
}
setFilter 會導(dǎo)致發(fā)送給 useQuery 的 Query Key 不同。從而觸發(fā)重取數(shù)據(jù)。導(dǎo)致 data 的變化從而重繪組件。
Colocation 共處
參照 Maintainability through colocation。
Queries 和 組件應(yīng)該共處在同一個功能下面,即還是應(yīng)該按照功能 / 組織代碼。關(guān)聯(lián)的代碼在相同的地方更有利于代碼的管理。代碼分層和抽象會導(dǎo)致關(guān)聯(lián)性降低,導(dǎo)致代碼維護(hù)性降低。
- src
- features
- Profile
- index.tsx
- queries.ts
- Todos
- index.tsx
- queries.ts
結(jié)構(gòu)
將Query Key 按照從寬泛到獨有安排。例如
['todos', 'list', { filters: 'all' }]
['todos', 'list', { filters: 'done' }]
['todos', 'detail', 1]
['todos', 'detail', 2]
使用 Query Key Factory
使用 factory 管理 Query Key 的關(guān)聯(lián)性
const todoKeys = {
all: ['todos'] as const,
lists: () => [...todoKeys.all, 'list'] as const,
list: (filters: string) => [...todoKeys.lists(), { filters }] as const,
details: () => [...todoKeys.all, 'detail'] as const,
detail: (id: number) => [...todoKeys.details(), id] as const,
}