#2 React Query: Infinite Scroll

kevin-uehara

Kevin Toshihiro Uehara

Posted on January 28, 2024

#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

💖 💪 🙅 🚩
kevin-uehara
Kevin Toshihiro Uehara

Posted on January 28, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Micro Frontends with Vite and Bit
javascript Micro Frontends with Vite and Bit

March 13, 2024

#2 React Query: Infinite Scroll
javascript #2 React Query: Infinite Scroll

January 28, 2024

Power of Javascript Array Methods 🔍🛠️
javascript Power of Javascript Array Methods 🔍🛠️

September 29, 2023

Better API Calls 🎣
javascript Better API Calls 🎣

September 24, 2023

Mastering JavaScript Date and Time 📆
javascript Mastering JavaScript Date and Time 📆

September 22, 2023