Query相关
这里只是简单的讲解了常用的功能,还有详细的东西请参考官网。
1. 基本使用
const {
isLoading,
status,
data,
} = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList
})
使用useQuery
生成一个请求对象,它接收很多参数,最重要的两个就是queryKey
和queryFn
,具体后面说明。
也可以这样写:
useQuery(queryKey, queryFn, otherOptions)
返回一个请求对象,可以从里面获取请求状态、数据等信息。默认情况下,组件渲染后就会自动请求。
2. useQuery
状态
大体分为请求状态
和请求器状态
(这名字我自己取的)。
2.1 请求状态
代表这次请求,某一个请求的状态。
status
:请求状态,注意返回的是字符串,'loading' | 'error' | 'success'
isLoading
:是否正在请求,默认是true
。这里有个小坑:
// 假设这个请求不需要渲染完成后立即请求
const posts = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
// 关闭自动请求
enabled: false
})
// 组件渲染完成后就一直在loading
// 可以增加条件判断,比如posts.isFetching && posts.isLoading,等价于`isInitialLoading`
<Spin spinning={posts.isLoading}>
{/** 内容 **/}
</Spin>
- 其他状态特殊情况下有用,这里就不赘述,需要的时候搜文档
2.2 请求器状态
指的是请求器的状态,他和请求状态是有区别的,主要就是fetchStatus
属性,它有几个值
paused
,请求被暂停,通常出现在组件渲染完成的时候,没有网络的情况。但是不同的network mode
配置值会不同。idle
:请求器空闲,什么事情都没做,通常是请求完成,无论成功或者失败fetching
:正在请求
所以这两种状态代表不同意义,比如:
status
已经success
但是,正在执行后台请求,此时fetchStatus
值为fetching
3. useQuery
配置项
3.1 queryKey
简单的来说就是一个标识符,给query
取一个唯一的名字,不过这个配置可以接收很多数据类型,所以甚至可以用来传递参数。
这个key
的作用,简单的来说,react-query
可以知道哪一个请求需要更新,是否过期。
// 可以是string[]
useQuery({ queryKey: ['something', 'special'], ... })
// 也可以加入对象,注意对象中的键顺序不一样是等价的
// 下面几个都是代表同一个key
useQuery({ queryKey: ['todos', { status, page }], ... })
useQuery({ queryKey: ['todos', { page, status }], ...})
useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... })
// 这样代表不一样的key
useQuery({ queryKey: ['todos', status, page], ... })
useQuery({ queryKey: ['todos', page, status], ...})
useQuery({ queryKey: ['todos', undefined, page, status], ...})
还有一点需要注意,如果queryFn
需要传递一个变量当做参数,那么你需要把这个变量加入queryKey
。道理很简单,react-query
通过key
来判断是否需要更新,有点像useEffect
的依赖数组。
function Todos({ todoId }) {
const result = useQuery({
// 所以这里需要加入todoId作为key的一部分
queryKey: ['todos', todoId],
// fetchTodoById依赖变量todoId
queryFn: () => fetchTodoById(todoId),
})
}
3.2 queryFn
queryFn
就是一个普通的异步函数,返回的值会作为data
放入。不过默认会给这个函数注入一些参数,在需要的时候可以使用。resolve
就会正常返回,thorw
错误就会导致status = error
下面都是常见的用法:
useQuery({ queryKey: ['todos'], queryFn: fetchAllTodos })
useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId) })
useQuery({
queryKey: ['todos', todoId],
queryFn: async () => {
const data = await fetchTodoById(todoId)
return data
},
})
useQuery({
queryKey: ['todos', todoId],
// react-query会传入一些额外的参数
queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]),
})
传给queryFn
的参数被称为QueryFunctionContext
,具体有下面这些属性
queryKey
:就是配置选项中的queryKey
pageParam
:当你使用useInfiniteQuery
的时候传递的参数signal
:主要就是取消请求meta
:配置选项中的meta
配置,自定义的一些信息
4. 并发请求
正常情况下直接同时启动请求,就能并发请求
function App () {
// 下面的请求就是并发的
const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams })
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects })
...
}
注意:,在React.Suspense
的表现不大一样,会使组件渲染被阻塞,所以如果要同时请求多个数据,最好是使用useQueries
function App({ users }) {
const userQueries = useQueries({
// 接收一个query配置数组
queries: users.map((user) => {
// 数组每一项,就是单独的useQuery的配置
return {
queryKey: ['user', user.id],
queryFn: () => fetchUserById(user.id),
}
}),
})
}
5. 依赖请求
有的请求需要前一个请求完成后再请求,或者需要等到某个数据有值后再请求,就可以使用enabled
配置控制
// 先请求用户id
const user = useQuery({
queryKey: ['user', email],
queryFn: getUserByEmail,
})
const userId = user.data?.id
// 再请求用户对应的项目
const projects = useQuery({
queryKey: ['projects', userId],
queryFn: getProjectsByUser,
// 当userId存在的时候才会请求
enabled: !!userId,
})
enabled
还有个作用,直接写false
就默认关闭初始化请求,但是你想手动开启只能使用refetch
,但是不能传参数,所以最好不要直接设置。
/**
其实大部分情况你都没必要设置false,回想下你为什么不需要立即请求。
无非就是这个请求需要后续的某个操作触发,所以一定有变量、变化,所以监听这个变化就行了
下面是一个筛选器的例子,一般情况下初始状态是不需要请求的,只有设置了筛选项才会请求
*/
function Todos() {
const [filter, setFilter] = React.useState('')
const { data } = useQuery({
queryKey: ['todos', filter],
queryFn: () => fetchTodos(filter),
// 筛选项为空时不请求
enabled: !!filter
})
return (
<div>
// 选择了筛选项后就会请求
<FiltersForm onApply={setFilter} />
{data && <TodosTable data={data}} />
</div>
)
}
6. 分页请求
就是普通请求,就是传个分页参数而已
// 当前页码,改变页码,请求就会更新
const [page, setPage] = React.useState(0)
// 请求函数
const fetchProjects = (page = 0) => fetch('/api/projects?page=' + page).then((res) => res.json())
const {
isLoading,
isError,
error,
data,
isFetching,
isPreviousData,
} = useQuery({
queryKey: ['projects', page],
queryFn: () => fetchProjects(page),
// 使用这个配置,可以防止切换页请求时,频繁的`loading`切换
keepPreviousData : true
})
7. 无限请求
通常用在移动端列表,上拉加载更多的那种情况。这种情况都有共同的状态需要处理,比如下一页参数如何传,是否还有下一页,保存之前请求的数据等等。所以react-query
给了单独的hook
。
// useInfiniteQuery 除了包含useQuery的配置,还有独有的一些配置
const { data: {pages, pageParam} } = useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
7.1 data
useInfiniteQuery
返回中的data
只会包含pages
和pageParam
。
pages
是一个数组,包含请求回来的数据。
// res = [1, 2, 3]
pages = [ [1, 2, 3] ]
// res = {success: true, records: [1, 2, 3]}
pages = [ {success: true, records: [1, 2, 3]} ]
// res = null
pages = [null]
也就是每调一次fetchNextPage
都会把响应放进pages
,取值的时候注意嵌套
const {pages} = useInfiniteQuery() // 假设有个请求
/**
那么每次请求下一页,pages都会增加一项
[
// 第一次请求
{
// 业务响应
status: 'success',
records: [{}, {}, {}]
},
// 第二次请求
{
status: 'success',
records: [{}, {}, {}]
},
....
]
**/
<div>
{
// 注意取值结构,实际根据自己的业务来
pages.map(res => res.map(data => <div>{ data.value }</div>))
}
</div>
7.2 getNextPageParam
当调用fetchNextPage
时传递的参数规则,函数返回undefined
代表没有下一页,上一页的参数也是这样传。
/*
假设业务里面,分页是这样定义的
{
pages: 总共多少页
size: 每一页的大小
current: 当前页码
data: 页的数据
}
**/
useInfiniteQuery({
// ...
// lastPage就是useInfiniteQuery返回的pages的最后一项
getNextPageParam: (lastPage, pages) => {
// 当前页码等于总页数的时候就返回undefined,表明没有下一页了
// 否则查询参数就是当前页码加一
return lastPage.data.current === lastPage.data.pages ? undefined : {page: lastPage.data.current + 1}
},
// ...
})
有时候参数不止这些,可以直接给fetchNextPage
传递参数,但是会覆盖掉getNextPageParam
里的返回参数。
fetchNextPage({ pageParam: 50 })
7.3 refetch
无限请求的重试有点特殊,因为pages
里面包含了之前的请求结果,如果需要重新请求,那就只能按照顺序,依次重新请求。当然也提供了重新请求某一页的配置。
const { refetch } = useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
// only refetch the first page
refetch({ refetchPage: (page, index) => index === 0 })
8. initialData
和placeholder data
顾名思义就是设置初始数据,就是在请求还没发出或者请求还没响应的时候占位的初始值。
但是这两个有明显的区别:initialData
会被缓存,而placeholderData
不会。
/*
initialData可以设置staleTime过期时间,当过期时间到了之后,请求才会发出更新数据。
**/
const result = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/todos'),
initialData: initialTodos,
staleTime: 1000,
// 由于staleTime同时也配置数据本身的过期时间,为了不冲突可以选择下面的选项
initialDataUpdatedAt:时间戳
})
9.预请求
在useQuery
第一次自动请求前,先请求数据并缓存。
// a.tsx
useEffect(() => {
queryClient.prefetchQuery({
queryKey: ['prefetchQuery'],
queryFn:getData,
})
}, [])
// b.tsx
// 这里面第一次自动请求的时候就不会发请求,而是直接获取之前的预请求数据
const testPrefetch = useQuery({
queryKey: ['prefetchQuery'],
queryFn:getData,
})
同样预请求也可以设置staleTime
,如果过期了,query
仍然会重新请求。