DEV Community

Cover image for #2 React Query: Infinite Scroll
Kevin Toshihiro Uehara
Kevin Toshihiro Uehara

Posted on

#2 React Query: Infinite Scroll

Hi Folks!!! How are you? Fine? How have you been?

As I promissed on the first article about react query, where I talk about this state management tool that provide a lot of features as pagination, cache, auto refetch, pre-fetching and a lot of more.

In this article I will be talking about the amazing feature of react query, the infinite scroll.

Introduction

Probably you already saw this feature in every social media, for example, X (I prefer talk twitter, yet), or facebook, linkedin... Where we dont't have a pagination, but a infinit scroll the data it will be generated for you. Without you have buttons (next or previous) to use the pagination.

But the infinite scroll underneath is a type of pagination.

So with this in mind, let's see on the code!!!

Hands on

I will use the same project that I created in the previous article. So, I will not show how to install or configure the react query. If you don't know, I recommend that you read the first article, where I teach and show how to install, settings etc...

You can find here: https://dev.to/kevin-uehara/1-react-query-introducing-pagination-caching-and-pre-fetching-21p8

With this in mind I will pre-assume that you already have the project configured or you know the basics of react query.

I will use the same Fake API, to provide us the data. The JSON Placeholder, but this time, I will use the Todo endpoint.

For example, access: https://jsonplaceholder.typicode.com/todos?_pages=0&_limit=10

JSON placeholder page

So in the same project before, how I said, let's create the folder for our componennt Todos: src/Todo/index.tsx.

On other circumstances, I probably going to create the types.ts for our types. But we going to use only in this file. So I will create the type in our component. Also, I will add the MAX_POST_PAGE constant.

src/Todo/index.tsx

const MAX_POST_PAGE = 10;

interface TodoType {
  id: number;
  title: string;
}
Enter fullscreen mode Exit fullscreen mode

So we will have the limit of 10 and the type of our Todo, it will use only the id and title.

Now let's create the function to fetch our data:

const fetchTodos = async ({ pageParam }: { pageParam: number }) => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos?_pages=${pageParam}&_limit=${MAX_POST_PAGE}`
  );
  const todos = (await response.json()) as TodoType[];
  return todos;
};
Enter fullscreen mode Exit fullscreen mode

Notice that the function will receive the pageParam representating the pageNumber. I will receive as object and use the descruct.

So far our component will look like this:

const MAX_POST_PAGE = 10;

interface TodoType {
  id: number;
  title: string;
}

const fetchTodos = async ({ pageParam }: { pageParam: number }) => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos?_pages=${pageParam}&_limit=${MAX_POST_PAGE}`
  );
  const todos = (await response.json()) as TodoType[];
  return todos;
};

export const Todo = () => {
  return <></>;
Enter fullscreen mode Exit fullscreen mode

Now let's create the content of our Todo component.

Let's create an observer reference, using the useRef hook and pass IntersectionObserver type as generic, like this:

const observer = useRef<IntersectionObserver>();
Enter fullscreen mode Exit fullscreen mode

Observer is a desing pattern, as the definition:

Observer is a software design pattern that defines a one-to-many dependency between objects so that when an object changes state, all of its dependents are notified and updated automatically.
Enter fullscreen mode Exit fullscreen mode

The observer, like the own name said, it will be obersving the state of some object. If the dependency, update, the object that is listening (observing) it will be notified.

But you may be thinking 🤔 Why I'm explaining all of this concept. Well, we will need to use the observer to see if the user is on the final of our page to fetch the new data passing the next page param.

So, yep! How I said before, the infinite scroll is a different type of pagination 🤯

Mind Blow Gif

Let's use the hook useInfiniteQuery of react query. It's very similar of useQuery:

const { data, error, fetchNextPage, hasNextPage, isFetching, isLoading } =
    useInfiniteQuery({
      queryKey: ["todos"],
      queryFn: ({ pageParam }) => fetchTodos({ pageParam }),
      getNextPageParam: (lastPage, allPages) => {
        return lastPage.length ? allPages.length + 1 : undefined;
      },
    });
Enter fullscreen mode Exit fullscreen mode

We will destruct and get the data, error message, fetchNextpage function, if hasNextPage property, isFectching and isLoading states.

We will pass the key 'todos' on queryKey, the function fetchTodos on queryFn and create a function on getNextPageParam to get the next page, incrementing and validating if we have data.

Now let's create a function to observe if the user reached the end of the page.

const lastElementRef = useCallback(
    (node: HTMLDivElement) => {
      if (isLoading) return;

      if (observer.current) observer.current.disconnect();

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

      if (node) observer.current.observe(node);
    },
    [fetchNextPage, hasNextPage, isFetching, isLoading]
  );
Enter fullscreen mode Exit fullscreen mode

Don't worry if you don't understand this function now. But read with calmly.

We will receive the node, some element div to observe.

First I verify if the state isLoading, if is yes, I simple return nothing and exit of the function.

Now I verify if I already have the instance of IntersectionObserver. If already have, I disconnect, because I dont't want to create multiple instances of observers.

Now If we don't have. let's intanciante the with new IntersectionObserver() passing the entries as parameters of arrow function. Now we will validate if the page is intersecting, has next page and is not fetching.
If all this contitions is validated, i will call the fetchNextPage() returned by the useInfiniteQuery function.

Now let's pass the observe reference the node.

And that's it! A little monster, it's not? But if we read calmly we see that's is not so complicated.

SpongeBob Tired gif

Now I will format our data to simplify our data, using the reduce:

const todos = useMemo(() => {
    return data?.pages.reduce((acc, page) => {
      return [...acc, ...page];
    }, []);
  }, [data]);
Enter fullscreen mode Exit fullscreen mode

Now Let's validate and return the possible states and return the values:

 if (isLoading) return <h1>Loading...</h1>;

 if (error) return <h1>Error on fetch data...</h1>;

return (
    <div>
      {todos &&
        todos.map((item) => (
          <div key={item.id} ref={lastElementRef}>
            <p>{item.title}</p>
          </div>
        ))}

      {isFetching && <div>Fetching more data...</div>}
    </div>
Enter fullscreen mode Exit fullscreen mode

In resume we will have this component:

src/Todos/index.tsx

import { useCallback, useMemo, useRef } from "react";
import { useInfiniteQuery } from "react-query";

const MAX_POST_PAGE = 10;

interface TodoType {
  id: number;
  title: string;
}

const fetchTodos = async ({ pageParam }: { pageParam: number }) => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos?_pages=${pageParam}&_limit=${MAX_POST_PAGE}`
  );
  const todos = (await response.json()) as TodoType[];
  return todos;
};

export const Todo = () => {
  const observer = useRef<IntersectionObserver>();

  const { data, error, fetchNextPage, hasNextPage, isFetching, isLoading } =
    useInfiniteQuery({
      queryKey: ["todos"],
      queryFn: ({ pageParam }) => fetchTodos({ pageParam }),
      getNextPageParam: (lastPage, allPages) => {
        return lastPage.length ? allPages.length + 1 : undefined;
      },
    });

  const lastElementRef = useCallback(
    (node: HTMLDivElement) => {
      if (isLoading) return;

      if (observer.current) observer.current.disconnect();

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

      if (node) observer.current.observe(node);
    },
    [fetchNextPage, hasNextPage, isFetching, isLoading]
  );

  const todos = useMemo(() => {
    return data?.pages.reduce((acc, page) => {
      return [...acc, ...page];
    }, []);
  }, [data]);

  if (isLoading) return <h1>Carregando mais dados...</h1>;

  if (error) return <h1>Erro ao carregar os dados</h1>;

  return (
    <div>
      {todos &&
        todos.map((item) => (
          <div key={item.id} ref={lastElementRef}>
            <p>{item.title}</p>
          </div>
        ))}

      {isFetching && <div>Carregando mais dados...</div>}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Now on the main.tsx I will replace the App.tsx of our previous example to render our Todo component:

src/main.tsx

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <Todo />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  </React.StrictMode>
Enter fullscreen mode Exit fullscreen mode

Now will have the result:

Infinite Scroll Example with Todos

AND NOWWW WE HAVE THE INFINE SCROLL!!! How amazing, is it?

So peole, that's it!!!
I hope you liked of this second article of this amazing tool react query.

Stay well, always.
Thank you so much to read until here.

Dog smile gif

Contacts:
Youtube: https://www.youtube.com/@ueharakevin/
Linkedin: https://www.linkedin.com/in/kevin-uehara/
Instagram: https://www.instagram.com/uehara_kevin/
Twitter: https://twitter.com/ueharaDev
Github: https://github.com/kevinuehara

Top comments (4)

Collapse
 
guadalazzo profile image
Guadalupe Jazmin Lazzo

is a great article,
just one thing missing:
const { data, error, fetchNextPage, hasNextPage, isFetching, isLoading } =
useInfiniteQuery({
(...)
initialPageParam: 0,
getNextPageParam: (lastPage, allPages) => {
return lastPage.length ? allPages.length + 1 : undefined;
},
});
initialPageParam is missing

Collapse
 
rajaerobinson profile image
Rajae Robinson

Great article. In React Query v5 you can use maxPages prop with infinite queries to limit the number of stored and refetched pages

Collapse
 
ricardogesteves profile image
Ricardo Esteves • Edited

Great article @kevin-uehara !!! I really enjoyed it, well structured and really insightful. 👌

I will share it if it's ok with you.

Collapse
 
adophilus profile image
Adophilus

Nice article. I want to implement infinite scroll in a project I'm working on but I've got one question. Why did you set the ref on each element in the list you're rendering and not just place it a div that's at the bottom of the list?