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
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;
}
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;
};
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 <></>;
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>();
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.
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 🤯
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;
},
});
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]
);
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.
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]);
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>
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>
);
};
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>
Now will have the result:
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.
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 (5)
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
Great article. In React Query v5 you can use maxPages prop with infinite queries to limit the number of stored and refetched pages
Great article @kevin-uehara !!! I really enjoyed it, well structured and really insightful. 👌
I will share it if it's ok with you.
Also have a look at QueryClient configuration to improve items speed loading
dhiwise.com/post/optimizing-perfor...
Further, good answer on SO
stackoverflow.com/questions/712861...
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?