概述
React Query 时一个异步状态管理库,核心的概念有三个:
- 查询(Queries)
- 修改(Mutations)
- 查询错误处理(Query Invalidation)
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
import { getTodos, postTodo } from "../my-api";
// 创建一个 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"));
默认配置
- 默认情况下,通过useQuery或useInfiniteQuery生成的查询实例会将缓存的数据视为过时(stale)的。
- 失败的查询将静默重试 3 次,在捕获并向 UI 显示错误之前,会有指数级的后退延迟(exponential backoff delay)。
所谓指数级的后退延迟,是指每次重试时,会指数级增加等待时间,也即是指数级的降低访问频率。
要更改这一点,你可以将查询的默认
retry
和retryDelay
选项更改为3之外的其他值或指数后退函数。
- 默认情况下,查询结果在结构上是共享的,以检测数据是否确实发生了更改。如果没有,则数据的引用保持不变。
查询 Queries
FetchStatus
在任何给定时刻,查询只能处于以下状态之一:
-
isLoading
或者status === 'loading'
- 查询暂时还没有数据 -
isError
或者status === 'error'
- 查询遇到一个错误 -
isSuccess
或者status === 'success'
- 查询成功,并且数据可用
除了status
字段,result
对象,还会有一个额外的fetchStatus
属性,它有以下选项:
-
fetchStatus === 'fetching'
- 正在查询中 -
fetchStatus === 'paused'
- 查询想要获取,但它被暂停了。在网络模式中阅读更多相关信息 -
fetchStatus === 'idle'
- 该查询处于闲置状态
为什么有两种表示状态的东西(status
/fetchStatus
)?
后台刷新和数据过期重试(stale-while-revalidate)的逻辑使status
和fetchStatus
的所有组合成为了可能。比如说:
- 一个
state='success'
的查询通常处于fetchStatus='idle'
状态。但如果同时有后台重新获取动作,它也可能为fetchStatus='fetching'
状态。 - 一个没有数据的查询通常处于
status='loading'
状态和fetchStatus='loading
状态。如果同时无网络连接,它也可能为fetchStatus='paused'
状态。
所以请记住,一个查询可以处于fetchStatus='loading'
状态,但没有实际的在获取数据。 如何理清两者关系?这里有一个简单的经验法则:
-
status
告诉我们有关data
的状态:有或者没有? -
fetchStatus
告诉我们有关queryFn
的状态:在执行还是没在执行?
有效的使用 React Query Keys
https://tkdodo.eu/blog/effective-react-query-keys
查询键值是 hash 决定的!
这意味着,不管对象中键值的顺序如何,以下所有查询都被认为是相等的:
useQuery(['todos', { status, page }], ...);
useQuery(['todos', { page, status }], ...);
useQuery(['todos', { page, status, other: undefined }], ...);
但是,以下查询键值不相等。这些数组项的顺序很重要,因为它们的散列信息并不相同!
useQuery(['todos', status, page], ...);
useQuery(['todos', page, status], ...);
useQuery(['todos', undefined, page, status], ...);
useQuery 和 useInfiniteQuery 共享缓存,不能使用同样的 key。因为两种数据请求结构不同。
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),
})
}
自动重取数据
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
会导致发送给 useQuery
的 Query Key 不同。从而触发重取数据。导致 data 的变化从而重绘组件。
Colocation 共处
参照 Maintainability through colocation。
Queries 和 组件应该共处在同一个功能下面,即还是应该按照功能 / 组织代码。关联的代码在相同的地方更有利于代码的管理。代码分层和抽象会导致关联性降低,导致代码维护性降低。
- src
- features
- Profile
- index.tsx
- queries.ts
- Todos
- index.tsx
- queries.ts
结构
将Query Key 按照从宽泛到独有安排。例如
['todos', 'list', { filters: 'all' }]
['todos', 'list', { filters: 'done' }]
['todos', 'detail', 1]
['todos', 'detail', 2]
使用 Query Key Factory
使用 factory 管理 Query Key 的关联性
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,
}