如何使用 React Query 做下拉数据自动刷新?

开发 前端
本文我们讲述了 React Query 中用于无限查询 API useInfiniteQuery() 的使用。通过循序渐进的 3 个案例,最终实现了一个下拉到底后自动新数据的交互效果,还是比较好实现的。

useInfiniteQuery() API

看名字就能猜出来,useInfiniteQuery() 是专门用来应付无限查询场景的。不仅如此,useInfiniteQuery() API 能力也是基于 useQuery() 的。

之前的文章中我们介绍了 useQuery() 的核心 API,为了找回印象,我们在此贴出来:

import { useQuery } from 'react-query'

const {
  data,
  error,
  isError,
  isFetching,
  isLoading,
  isRefetching,
  isSuccess,
  refetch,
} = useQuery(queryKey, queryFn?, {
  enabled,
  onError,
  onSuccess,
  refetchOnWindowFocus,
  retry,
  staleTime,
})

如果我们把这些 API 简化如下:

const {
  ...result,
} = useQuery(queryKey, queryFn?, {
  ...options,
})

useInfiniteQuery() 其实就是在 useQuery() 基础之上增添一些无限查询场景的参数:

const {
  fetchNextPage,
  fetchPreviousPage,
  hasNextPage,
  hasPreviousPage,
  isFetchingNextPage,
  isFetchingPreviousPage,
  ...result
} = useInfiniteQuery(queryKey, ({ pageParam = 1 }) => fetchPage(pageParam), {
  ...options,
  getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
})

如你所见,增加的 API 其实就是跟上一页/下一页查询动作相关的参数,相比较于自己组装 的分页查询能力的 useQuery(),useInfiniteQuery() 需要配置上一页/下一页的参数获取函数,并提供了相应的查询调用能力,更加自动化和便捷。

当然,增加的不只是参数,还有 2 处:

一个是 queryFn 参数的入参,多了一个名为 pageParam 的参数。

pageParam 表示当前页数。这个值是每次 useInfiniteQuery() 调用时,通过 getNextPageParam()/getPreviousPageParam() 返回值自动获取并传入 queryFn 的。

第二个还有返回值的数据结构,即 data。

const { data } = useInfiniteQuery()

原来 data 就是表示内部请求方法的返回值。而 useInfiniteQuery() 的返回 data 因为要包含多页数据(展示旧数据时,还要持有旧数据),因此 data 变更为:

data: { pages: TData[], pageParams: unknown[] }

pages 很好理解,就是用来承载过程中请求到的多页数据;pageParams 则是每个页面当时在做数据获取时使用的查询参数。

简单一例

当然语言上说得再多,也是苍白无力的,实践出真知。这里我们就举一个简单的例子说明 useInfiniteQuery() 的使用。

首先,我们先创建一个获取数据的请求函数(使用 Fetch API)。

const getPosts = async (pageParam) => {
  return fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam.page}&_limit=${pageParam.size}`).then(res => res.json())
}

接着,使用 useInfiniteQuery() 请求数据:

function Example() {
  const {
    isLoading,
    isError,
    error,
    data,
  } = useInfiniteQuery(
    'posts',
    ({ pageParam }) => getPosts(pageParam),
    {
      getNextPageParam: (lastPage, allPages) => ({ page: allPages.length + 1, size: 6 }),
      refetchOnWindowFocus: false, // Prevent refetching on window focus
    }
  )
  
  // ...
}

增加下加载中或出现异常时的处理逻辑。

function Example() {
  // ...
  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (isError) {
    return <div>Error: {error.message}</div>
  }
  
  // ...
}

最后渲染分页数据。

function Example() {
  // ...

  return (
    <div>
      <ol>
        {/* (1) */}
        {data.pages.map((page) => (
          {page.map((post) => (
            <li key={post.id}>{post.title}</li>
          ))}
        ))}
      </ol>
      
      {/* (2) */}
      <button onClick={() => fetchNextPage()}>More</button>
    </div>
  )
}
  1. 遍历 data.pages 中所有页面数据,渲染出来
  2. 使用 fetchNextPage() 函数加载更多(实际上即“下一页”)数据

浏览器访问,不幸运是,报错了。

图片图片

没关系,点击查看报错处。

图片图片

发现是是由于首次请求接口时,pageParam 参数是 undefined,这就需要我们给 pageParam 一个默认值。

- const getPosts = async (pageParam) => {
+ const getPosts = async (pageParam = { page: 1, size: 6 }) => {
  return fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam.page}&_limit=${pageParam.size}`).then(res => res.json())
}

再次访问页面,发现我们成功请求到了第一页数据。

图片图片

接下来点击“More”按钮,发现发起了到第二页数据的请求。

图片图片

图片图片

这里逻辑是先调用 getNextPageParam() 函数获得请求参数。getNextPageParam() 函数的类型声明如下:

getNextPageParam: (lastPage, allPages) => unknown | undefined

第一个参数就是上一页数据,第二个参数即到目前为止请求到的所有数据。对照本例,当我们第一次点击“More”按钮时,返回的值为 { page:2, size:6 }。然后,再发起 getPosts 调用并这个参数,于是我们便发起了第二页的数据请求,并最终得到了数据。

下面,我们再给“More”按钮增加一个加载状态——我们可以使用通用的 isFetching 状态,但这里我们使用更具体的 isFetchingNextPage 状态。

function Example() {
  const {
    isLoading,
+   isFetchingNextPage,
    isError,
    error,
    data,
  } = useInfiniteQuery()

添加给按钮。

- <button notallow={() => fetchNextPage()}>More</button>
+ <button disabled={isFetchingNextPage} notallow={() => fetchNextPage()}>More</button>

查看效果。

图片图片

这样,我们就能在请求下一页数据的时候禁用按钮点击,避免用户误触。

再来一例

以上案例中,我们是通过 getNextPageParam() 所提供的第二个参数 allPages 来获得下一页页码的。

getNextPageParam: (lastPage, allPages) => ({ page: allPages.length + 1, size: 6 })

这是因为返回的第一页数据总并未包含下一页信息。

图片图片

我们再稍微修改下

const getPosts = async (pageParam = { page: 1, size: 6 }) => {
  return fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam.page}&_limit=${pageParam.size}`).then(res => {
    const total = res.headers.get('X-Total-Count')

    return res.json().then(data => {
      return {
        total,
        data,
        hasMore: pageParam.page * pageParam.size < total
      }
    })
  })
}

我们将返回数据放在 data 属性中,并返回数据总数(total)以及是否还有数据(hasMore)。

有了这些信息,我们就可以有了更加细腻的交互了。

下面,修改渲染逻辑。

function Example() {
  // ...
  
  return (
    <div>
      <ol>
        {data.pages.map((page) => (
          <>
            {page.data.map((post) => (
              <li key={post.id}>{post.title}</li>
            ))}
          </>
        ))}
      </ol>
      <p>总共 <strong>{data.pages[0].total}</strong> 条数据</p>
      {
        data.pages[data.pages.length - 1].hasMore ? (
          <button disabled={isFetchingNextPage} onClick={() => fetchNextPage()}>More</button>
        ) : <span>--- 我是有底线的 ---</span>
      }
    </div>
  )
}

查看初始加载效果。

图片图片

查看最终效果。

图片图片

不过,按钮显隐逻辑还是有些冗余。

{
  data.pages[data.pages.length - 1].hasMore ? (
    <button disabled={isFetchingNextPage} onClick={() => fetchNextPage()}>More</button>
  ) : <span>--- 我是有底线的 ---</span>
}

这一点 useInfiniteQuery 也帮我们想到了,便提供了一个 hasNextPage 供我们使用。

{
-  data.pages[data.pages.length - 1].hasMore ? (
+  hasNextPage ? (
    <button disabled={isFetchingNextPage} notallow={() => fetchNextPage()}>More</button>
  ) : <span>--- 我是有底线的 ---</span>
}

hasNextPage 为 true 表示有下一页数据,为 false 表示没有数据了。

不过 hasNextPage 的值是受到 getNextPageParam() 返回值影响的——当 getNextPageParam() 返回 undefined 时,hasNextPage 值为 false,否则为 true。

因此,我们修改下 getNextPageParam() 的判断逻辑。

getNextPageParam: (lastPage, allPages) => {
  return lastPage.hasMore 
    ? { page: allPages.length + 1, size: 50 } 
    : undefined
},

如此,我们再来查看下效果。

图片图片

发现没有数据的时候,同样也不会展示“More”按钮了。大功告成!

下拉拉取新数据

当然,以上通过点击“More”按钮加载新数据的方式还是太麻烦了一些。如果我们在网页下拉到底部的时候自动获取数据不是更好吗?

下面就来实现一下。

我们将借助 IntersectionObserver API[4] + 底部一个 .loadMore 元素来实现。

首先,将 “More” 按钮部分的代码替换成:

<div className="loadMore" style={{ height: '30px', lineHeight: '30px' }} ref={loadMoreRef}>
  {
    isFetchingNextPage 
      ? <span>Loading...</span> 
      : <span>--- 我是有底线的 ---</span>
  }
</div>

注意,我们同时通过 loadMoreRef 拿到 DOM 元素。

然后,我们监听 .loadMore 元素,一旦出现在屏幕中,就调用 fetchNextPage() 获取下一页数据。

useEffect(() => {
  const observer = new IntersectionObserver((entries) => {
    if (entries[0].isIntersecting && hasNextPage) {
      fetchNextPage();
    }
  });

  if (loadMoreRef.current) {
    observer.observe(loadMoreRef.current);
  }

  return () => observer.disconnect();
}, [hasNextPage, fetchNextPage]);

来查看效果。

图片图片

完美。

最后,再把完整代码贴出来,方便大家学习。

import { useEffect, useRef } from 'react'
import { QueryClient, QueryClientProvider, useInfiniteQuery } from 'react-query'

// Create a client
const queryClient = new QueryClient()

export default function App() {
  return (
    // Provide the client to your App
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

const getPosts = async (pageParam = { page: 1, size: 25 }) => {
  return fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam.page}&_limit=${pageParam.size}`).then(res => {
    const total = res.headers.get('X-Total-Count')

    return res.json().then(data => {
      return {
        total,
        data,
        hasMore: pageParam.page * pageParam.size < total
      }
    })
  })
}

function Example() {
  const {
    isLoading,
    isFetchingNextPage,
    hasNextPage,
    isError,
    error,
    data,
    fetchNextPage
  } = useInfiniteQuery(
    'posts',
    ({ pageParam }) => getPosts(pageParam),
    {
      getNextPageParam: (lastPage, allPages) => {
        return lastPage.hasMore ? { page: allPages.length + 1, size: 25 } : undefined
      },
      refetchOnWindowFocus: false, // Prevent refetching on window focus
    }
  )

  const loadMoreRef = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting && hasNextPage) {
        fetchNextPage();
      }
    });

    if (loadMoreRef.current) {
      observer.observe(loadMoreRef.current);
    }

    return () => observer.disconnect();
  }, [hasNextPage, fetchNextPage]);


  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (isError) {
    return <div>Error: {error.message}</div>
  }

  return (
    <div>
      <p>总共 <strong>{data.pages[0].total}</strong> 条数据</p>
      <ol>
        {data.pages.map((page) => (
          <>
            {page.data.map((post) => (
              <li key={post.id}>{post.title}</li>
            ))}
          </>
        ))}
      </ol>
      <div className="loadMore" style={{ height: '30px', lineHeight: '30px' }} ref={loadMoreRef}>
        {
          isFetchingNextPage ? <span>Loading...</span> : <span>--- 我是有底线的 ---</span>
        }
      </div>
    </div>
  )
}

总结

本文我们讲述了 React Query 中用于无限查询 API useInfiniteQuery() 的使用。

通过循序渐进的 3 个案例,最终实现了一个下拉到底后自动新数据的交互效果,还是比较好实现的。

当然,本文只是以“下一页”举例,“上一页”与此同理。

希望本位讲述的内容能够对你的工作有所帮助。感谢阅读,再见。

参考资料

[1]React Query 是做什么的?: https://juejin.cn/post/7378015213348257855

[2]一个数据获竟然被 React Query 玩出这么多花样来!: https://juejin.cn/post/7380342160581918731

[3]React Query 的 useQuery 竟也内置了分页查询支持!: https://juejin.cn/post/7380569775686746151

[4]IntersectionObserver API: https://ruanyifeng.com/blog/2016/11/intersectionobserver_api.html

责任编辑:武晓燕 来源: 写代码的宝哥
相关推荐

2022-08-03 09:11:31

React性能优化

2019-11-22 08:40:19

ProtobufGo编程语言

2015-04-22 10:57:22

androidSwipeRefres

2013-07-17 16:33:02

下拉刷新listvie滚动到底部加载Android开发学习

2015-03-23 18:11:39

UITableViewswift下拉刷新

2016-08-05 17:01:09

AndroidRecyclerVie下拉刷新

2022-03-30 09:43:19

jscodeshif自动化重构开发

2019-10-08 11:10:18

React自动保存前端

2023-06-16 09:08:39

ReactContextRFC

2023-11-27 08:24:57

FormikReact

2015-05-13 09:36:18

js模拟手机下拉刷新

2021-12-01 10:02:57

鸿蒙HarmonyOS应用

2012-04-12 10:19:08

Ajax.NET

2023-05-26 06:30:56

2020-11-10 14:02:01

数据滚动SimulinkC语言

2022-04-14 08:00:00

Cypress测试开发

2022-12-07 11:21:30

Reactdiff

2009-06-29 09:00:42

Hibernate的Q

2021-06-01 09:27:52

视频动画Remotion

2022-07-18 09:01:58

React函数组件Hooks
点赞
收藏

51CTO技术栈公众号