DoneDeal0
DoneDeal0

Reputation: 6267

React-query: how to paginate with useInfiniteQuery

I would like to use react-query's useInfiniteQuery. Everything seems clear but the pagination, part.

The documentation exemple is:

export default () => {
  const {
    status,
    data,
    error,
    isFetching,
    isFetchingMore,
    fetchMore,
    canFetchMore,
  } = useInfiniteQuery(
    'projects',
    async (key, nextId = 0) => {
      const { data } = await axios.get('/api/projects?cursor=' + nextId)
      return data
    },
    {
      getFetchMore: lastGroup => lastGroup.nextId,
    }
  )

I understand what it does. projects is the id of the query, used for the cache, and then there is the function, that performs an api call. Now what about the nextId variable? It starts at zero, so the backend will know it must only return results from 0 to x (can we also pass a dynamic limit by the way?). But when fetchMore() - the function to trig the query again - is called, how on earth does useInfiniteQuery knows it should increment nextId?

Upvotes: 18

Views: 42611

Answers (4)

Subroto Biswas
Subroto Biswas

Reputation: 641

import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import axios from "axios";

interface Post {
  id: number;
  title: string;
  body: string;
  userId: number;
}

interface PostQuery {
  pageSize: number;
}

const usePosts = (query: PostQuery) =>
  useInfiniteQuery<Post[], Error>({
    queryKey: ["posts", query],
    queryFn: ({ pageParam = 1 }) =>
      axios
        .get("https://jsonplaceholder.typicode.com/posts", {
          params: {
            _start: (pageParam - 1) * query.pageSize,
            _limit: query.pageSize,
          },
        })
        .then((res) => res.data),
    staleTime: 1 * 60 * 1000, // 1m
    keepPreviousData: true, // Keep data of current page instade of showing loading
    getNextPageParam: (lastPage, allPages) => {
      // 1->2
      return lastPage.length > 0 ? allPages.length + 1 : undefined;
    },
  });

export default usePosts;

Upvotes: 0

wobsoriano
wobsoriano

Reputation: 13462

I'm trying to make an infinite scroll with rick and morty API and here's how I implemented it:

Your function (getCharacters) should accept a key and a prop (nextPage) and returns an object that includes the data (data) from your fetch and the page identifier (nextPage) plus 1.

const getCharacters = async (key, nextPage = 1) => {
  const res = await fetch(`${BASE_URL}/character/?page=${nextPage}`);
  const { results } = await res.json();
  return {
    data: results,
    nextPage: nextPage + 1,
  };
};

const { status, data, fetchMore } = useInfiniteQuery(
  ['characters'],
  getCharacters,
  {
    getFetchMore: (lastGroup, allGroups) => lastGroup.nextPage, // nextPage because we named it nextPage
  }
);

const paginatedData = [];
data.forEach((page) => {
  page.data.forEach((char) => {
    paginatedData.push(char);
  });
});

console.log(paginatedData) // The whole data

Update:

A better example using Rick and Morty API

async function getCharacters(page: number): Promise<Response> {
    const response = await fetch('https://rickandmortyapi.com/api/character?page=' + page)
    const characters = (await response.json())
    return characters;
}

const { ... } = useInfiniteQuery(['characters'], ({ pageParam = 1 }) => getCharacters(pageParam), {
  getNextPageParam: (lastPage) => {
    const nextUrl = lastPage.info.next
    if (nextUrl) {
      // Return next page number
      return Number(nextUrl.charAt(nextUrl.length - 1))
    }
    // Return false means no next page
    return false
  }
})

Upvotes: 9

Anita
Anita

Reputation: 3166

The steps are:

  1. Waiting for useInfiniteQuery to request the first group of data by default.
  2. Returning the information for the next query in getNextPageParam.
  3. Calling fetchNextPage function.

Reference https://react-query.tanstack.com/guides/infinite-queries

Example 1 with rest api

  const fetchProjects = ({ pageParam = 0 }) =>
     fetch('/api/projects?cursor=' + pageParam)
 
   const {
     data,
     isLoading,
     fetchNextPage,
     hasNextPage,
   } = useInfiniteQuery('projects', fetchProjects, {
     getNextPageParam: (lastPage) => {
       // lastPage signature depends on your api respond, below is a pseudocode
       if (lastPage.hasNextPage) {
         return lastPage.nextCursor;
       }
         return undefined;
     },
   })

Example 2 with graphql query (pseudocode)

  const {
    data,
    fetchNextPage,
    isLoading,
  } = useInfiniteQuery(
    ['GetProjectsKeyQuery'],
    async ({ pageParam }) => {
      return graphqlClient.request(GetProjectsQuery, {
        isPublic: true, // some condition/variables if you have
        first: NBR_OF_ELEMENTS_TO_FETCH, // 10 to start with
        cursor: pageParam,
      });
    },
    {
      getNextPageParam: (lastPage) => {
         // pseudocode, lastPage depends on your api respond
        if (lastPage.projects.pageInfo.hasNextPage) {
          return lastPage.projects.pageInfo.endCursor;
        }
        return undefined;
      },
    },
  );

react-query will create data which contains an array called pages. Every time you call api with the new cursor/page/offset it will add new page to pages. You can flatMap data, e.g:

const projects = data.pages.flatMap((p) => p.projects.nodes)

Call fetchNextPage somewhere in your code when you want to call api again for next batch, e.g:

  const handleEndReached = () => {
    fetchNextPage();
  };

Graphql example query:

add to your query after: cursor:

  query GetProjectsQuery($isPublic: Boolean, $first: Int, $cursor: Cursor) {
  projects(
    condition: {isPublic: $isPublic}
    first: $first
    after: $cursor
  ) ...

Upvotes: 7

Ricardo Olarte
Ricardo Olarte

Reputation: 21

The function getFetchMore() has a lastGroup parameter. With this param, you have to check if your response (lastGroup parameter) has more data to fetch or not. In this case, I return a page variable, which will be the next page to fetch when you call fetchMore() and our fetchPlanets() async function gets this page variable as a parameter to concat() the new api_url.

I had a problem with the refetch on window focus because it had an issue to fetch the next group (page). So, I pass refetchOnWindowFocus: false in the options parameter.

const fetchPlanets = async (key, page = '1') => {
  const api_url = `https://swapi.dev/api/planets/?page=${page}`;
  console.log(api_url);
  const res = await fetch(api_url);
  return res.json();
};

const Planets = () => {
  const { status, data, isFetching, isFetchingMore, fetchMore, canFetchMore } = useInfiniteQuery(
    ['planets'],
    fetchPlanets,
    {
      refetchOnWindowFocus: false,
      onSuccess: () => console.log('data fetch with no problemo!'),
      onError: () => console.log('data fetch error!'),
      getFetchMore: (lastGroup) => {
        lastGroup.page = lastGroup.next?.split('=')[1];
        return lastGroup.page;
      },
    },
  );

  console.log('data', data);

Upvotes: 2

Related Questions