#2 React Query: Infinite Scroll
Tags: javascript, webdev, typescript, react
Published: 2024-01-28 22:24:49

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

As I promissed on the first [article about react query](https://dev.to/kevin-uehara/1-react-query-introducing-pagination-caching-and-pre-fetching-21p8), 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9y6rucd71oxbvo40ffnk.png)

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`
```tsx
const MAX_POST_PAGE = 10;

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

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:

```tsx
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;
};
```

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:

```tsx
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 <></>;
```

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:

```tsx
const observer = useRef<IntersectionObserver>();
```

`Observer` is a `desing pattern`, as the definition:

```txt
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.
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d3up07fyh171vu5l09jg.gif)


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

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

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.

```tsx
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]
  );
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9raid3pbz6659q8ohtto.gif)

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

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

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

```tsx
 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>
```

In resume we will have this component:

`src/Todos/index.tsx`

```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>
  );
};
```

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

`src/main.tsx`
```tsx
ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <Todo />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  </React.StrictMode>
```

Now will have the result:

![Infinite Scroll Example with Todos](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pur0qtx2ppk5xzdfy0wk.gif)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vnl93xoyp5btfy3jxv1a.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