likes
comments
collection
share

react-query手把手教程15-无限加载数据

作者站长头像
站长
· 阅读数 17

该系列其他文章可以点击查看专栏👉🏻👉🏻react-query手把手教程系列

场景

在前面的章节章,讲述了如何使用react-query进行更好的分页操作。但是现在越来越多的网站已经采用无限流模式,比如你在花瓣网搜索“大橙子”,把页面滑到底部,并没有展示出分页器,而是直接为你展示出了新的搜索结果。

react-query手把手教程15-无限加载数据

接下来会介绍react-query专门为解决该种情况下的方案useInfiniteQuery

实践

useInfiniteQuery

useInfiniteQueryuseQuery在大多数的设计上是一致的,包括都可以传入查询键和查询函数,并且会返回后端数据(data), 是否第一次获取数据中(isLoading), 是否错误(isError), 是否正在加载数据中(isFetching)等,但是依旧还是有一些区别的地方。

首先useInfiniteQuery钩子的查询函数将会接收到一个pageParam的参数,该参数为当前的翻页状态,你可以通过这个参数,获取相关的数据。这里还是以拉取github issue列表接口为例:

const fetchInfiniteIssues = async ({queryKey, pageParam = 1}) => {
  const [issues, org, repo] = queryKey
  const response = await fetch(
    `https://api.github.com/repos/${org}/${repo}/issues?page=${pageParam}`
  )


  const json = await response.json()
  return json
}

但是这个pageParam是从哪里来的呢?这就需要介绍一下useInfiniteQuery钩子的不同之处了,这个钩子需要传递一个getNextPageParam配置项,并且该配置项需要需要传入一个函数,pageParam就是getNextPageParam传入函数的返回值。

如果获取到的数据中,已经没有下一页的数据了,直接返回undefined即可,如果有数据那么直接返回当前第n页+1的结果即可:

const issuesInfiniteQuery = useInfiniteQuery(
  ['issues', org, repo],
  fetchInfiniteIssues,
  {
    getNextPageParam: (lastPage, pages) => {
      if (lastPage.length === 0) {
        return;
      }
      return pages.length + 1;
    }
  }
)

如果页面采用游标进行分页,此时需要返回下一页的游标即可,伪代码如下:

const issuesInfiniteQuery = useInfiniteQuery(
  ['issues', org, repo],
  fetchInfiniteIssues,
  {
    getNextPageParam: (lastPage, pages) => {
      return lastPage.nextCursor;
    }
  }
)

该钩子在返回值的结构与useQuery也有些许不同,数据将会被挂载在pages下。

渲染github issue列表的伪代码如下:

const InfiniteIssues = () => {
  const issuesInfiniteQuery = useInfiniteQuery(
    ['issues', org, repo],
    fetchInfiniteIssues,
    {
      getNextPageParam: (lastPage, pages) => {
        if (lastPage.length === 0) {
          return;
        }
        return pages.length + 1;
      }
    }
  )


  return (
    <div>
      {issuesInfiniteQuery.data.pages.map((page, index) => (
        <Fragment key={index}>
          {page.map(issue => (
            <Issue key={issue.id} issue={issue} />
          ))}
        </Fragment>
      ))}
    </div>
  )
}

目前已经搞定了展示以及参数问题,接下来就是怎么触发获取下一页的逻辑,react-query提供了fetchNextPage属性来支持该需求,一旦调用fetchNextPage方法后,getNextPageParam方法将会被触发,来确定下一个页面的参数是什么,并调用查询函数,最终将结果添加到现有的页面列表中。

加载更多按钮

你可以通过点击加载更多按钮来实现该需求,这是最简单的一种实现方式,伪代码如下:

const InfiniteIssues = () => {
  const issuesInfiniteQuery = useInfiniteQuery(
    // ...
  );


  return (
    <div>
      {issuesInfiniteQuery.data.pages.map((page, index) => (
        <Fragment key={index}>
          {page.map(issue => (
            <Issue key={issue.id} issue={issue} />
          ))}
        </Fragment>
      ))}
      <button onClick={
          () => issuesInfiniteQuery.fetchNextPage()
        }
        disabled={
          !issuesInfiniteQuery.hasNextPage ||
          issuesInfiniteQuery.isFetchingNextPage
        }
      >
        {issuesInfiniteQuery.isFetchingNextPage ?
          "正在加载下一页数据..." :
          "点击加载更多数据"}
      </button>
    </div>
  )
}

当用户点击了按钮后,将会调用fetchNextPage属性方法加载下一页,你可以通过isFetchingNextPage属性来知道当前是否在加载状态。

监听页面是否滚动到底部加载下一页数据

先实现一个react hook来监听当前页面是否已经滑动到底部:

function useScrollToBottomAction(container, callback, offset = 0) {
  const callbackRef = useRef(callback);


  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);


  useEffect(() => {
    const handleScroll = () => {
      let scrollContainer =
        container === document ? document.scrollingElement : container;
      if (
        scrollContainer.scrollTop + scrollContainer.clientHeight >=
        scrollContainer.scrollHeight - offset
      ) {
        callbackRef.current();
      }
    };


    container.addEventListener("scroll", handleScroll, {passive:true});


    return () => {
      container.removeEventListener("scroll", handleScroll);
    };
  }, [container, offset]);
}

使用该钩子你需要传入以下三个参数

  • container: 需要监听滚动的dom元素
  • callback:监听滚动到底部后回调的方法
  • offset: 如果你需要在离底部一定距离时就触发触底事件,你需要在这里设置相关的偏移量

编写好监听页面触底的钩子后,就需要实际使用它来实现相关功能了,伪代码如下:

const InfiniteIssues = () => {
  const issuesInfiniteQuery = useInfiniteQuery(
    // ...
  );


  useScrollToBottomAction(
    document,
    () => {
      // ①
      if (issuesInfiniteQuery.isFetchingNextPage) return;
      issuesInfiniteQuery.fetchNextPage();
    },
    500
  );


  return (
    <div>
      {issuesInfiniteQuery.data.pages.map((page, index) => (
        <Fragment key={index}>
          {page.map(issue => (
            <Issue key={issue.id} issue={issue} />
          ))}
        </Fragment>
      ))}
      {issuesInfiniteQuery.isFetchingNextPage && (
        <div>正在加载下一页数据...</div>
        )}
    </div>
  )

①:为了防止代码在加载过程中被多次触发,需要监听isFetchingNextPage属性为true时,不触发加载下一页的功能

重新获取数据

如果用户有重新获取数据的需求,react-query的策略将会是直接从第一页开始,一直获取到当前最近的页面。

使用这种更新策略的原因是因为不从初始的页面获取数据,假如是使用游标获取下一页的数据,一旦后端有新增和删除操作,下一页的游标就会发生变化,按照之前的游标重新刷新下一页就会出现问题。

双向无限查询

通常的分页查询中,后端都会告诉前端上一页以及下一页获取的参数,比如游标或者偏移量。

此时如果用户是在非第一页打开的无限流数据,比如第三页,此时你可以向useInfiniteQuery钩子传入getPreviousPageParam参数,通过fetchPreviousPage属性触发加载上一页的功能,同时通过isFetchingPreviousPage属性判断当前加载上一页接口是否正在请求数据,通过hasPreviousPage属性判断是否有上一页(其实是基于getPreviousPageParam配置项返回的参数)

github的伪代码如下:

const InfiniteIssues = () => {
  const issuesInfiniteQuery = useInfiniteQuery(
    ["issues", org, repo, {startingPage: 3}],
    fetchInfiniteIssues,
    {
      getNextPageParam: (lastPage, pages) => {
        if (lastPage.issueList.length === 0) {
          return;
        }
        return lastPage.pageParam + 1;
      },
      getPreviousPageParam: (firstPage, pages) => {
        if (firstPage.pageParam <= 1) {
          return;
        }
        return firstPage.pageParam - 1;
      }
    }
  );
 
  return (
    <div>
      <button onClick={
        () => issuesInfiniteQuery.fetchPreviousPage()
      }
      disabled={
        !issuesInfiniteQuery.hasPreviousPage || 
        issuesInfiniteQuery.isFetchingPreviousPage}
      >
        {issuesInfiniteQuery.isFetchingPreviousPage ? 
          "加载上一页数据中..." :
          "点击加载更多"}
      </button>


      {issuesInfiniteQuery.data.pages.map((page, index) => (
        <Fragment key={index}>
          {page.issueList.map(issue => (
            <Issue key={issue.id} issue={issue} />
          ))}
        </Fragment>
      ))}


      <button onClick={
        () => issuesInfiniteQuery.fetchNextPage()
      }
      disabled={
        !issuesInfiniteQuery.hasNextPage || 
        issuesInfiniteQuery.isFetchingNextPage}
      >
        {issuesInfiniteQuery.isFetchingNextPage ? 
          "加载下一页数据中..." :
          "点击加载更多"}
      </button>
    </div>
  )
}

这里笔者为了方便采用的是点击按钮的方式触发加载,你也可以写一个方法,如果用户向上滚动距离顶部一段距离后,可以触发此加载逻辑。

到本文为止,react-query的基本使用方法已经介绍完毕,接下来的文章中将会介绍一些进阶用法,比如如何在graphql中使用react-query,以及react-query如何与websocket结合使用等

转载自:https://juejin.cn/post/7204345146162724920
评论
请登录