react-query v4 学习笔记(二) (Query)

791 阅读7分钟

Query相关

这里只是简单的讲解了常用的功能,还有详细的东西请参考官网。

1. 基本使用

  const {
      isLoading,
      status,
      data,
  } = useQuery({ 
      queryKey: ['todos'], 
      queryFn: fetchTodoList 
    })

使用useQuery生成一个请求对象,它接收很多参数,最重要的两个就是queryKeyqueryFn,具体后面说明。

也可以这样写:

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只会包含pagespageParam

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. initialDataplaceholder 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仍然会重新请求。