Creating an Infinite Scroll Pagination with Custom React Hook and GraphQL

––– views

3 min read

The idea is that users will scroll down and won’t notice content loading in the background, making it feel like they can scroll down infinitely, if you go to any social media platform they do the same thing, no matter how long you scroll down there always seems to be new content being loaded.

Use on screen hook

All you need to do is pass a ref (a pointer to an HTML element) to this function, and it will check whether the observed element is currently in the user's viewport.

TS

import { useState, useEffect } from "react";
export default function useOnScreen(ref) {
const [isIntersecting, setIntersecting] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) =>
setIntersecting(entry.isIntersecting)
);
observer.observe(ref.current);
// Remove the observer as soon as the component is unmounted
return () => {
observer.disconnect();
};
}, []);
return isIntersecting;
}

Let’s take a SAAS platform I’m building as an example, on the search page, I initially load 5 projects, and at the end of the projects list there is a HTML div element, with the above hook attached to it.

TS

const projectsRef = useRef<HTMLDivElement>(null);
// allProjects represents the response list from server
<div ref={projectsRef}>
{loading ? <LoaderComponent /> : allProjects.length === 0 ? "You've reached the end!" : ""}
</div>

The observer hook in action

When a user reaches the end of the list I send a new request to the server requesting more records to be loaded.

We take the ref from the targeted div element and using the hook we create a boolean. So whenever the user reaches the element the below useEffect is activated.

🔥
If it’s not loading, and if we receive results back from the server, we increment page by one.

If we don’t receive any data from the server it means that all the projects for the given filters were already requested, and we don’t need to send another request

TS

const [currPage, setCurrPage] = useState(1);
const isOnScreen = useOnScreen(projectsRef)
useEffect(() => {
if (isOnScreen && !loading) {
if (allProjects.length) setCurrPage(currPage + 1)
}
}, [isOnScreen])

When currPage changes we send another request to server.

take is always 5, as we always want to take 5 new records.

💡
skip changes based on the currPage value.

TS

useEffect(() => {
if (projects?.length) {
getAllProjects({
variables: {
...values,
take: 5,
skip: (currPage - 1) * 5
}
})
}
}, [currPage])

Update the frontend state

Finally, we have state to keep track of the projects that need to be displayed. In the useEffect below we first check if there are already projects added if (projects) refers to the state value, if it’s not empty we just attach the new response of projects to the existing array setProjects([...projects, ...allProjects])

If it’s not already populated that means this was the first request and we just set the state to the response.

TS

const [projects, setProjects] = useState(projectsData?.projects)
useEffect(() => {
if (projects) {
if (projectsData?.projects) setProjects([...projects, ...allProjects])
} else {
setProjects(projectsData?.projects)
}
}, [projectsData])

If you’re wondering I am using Apollo GraphQL for querying, but if you understand the concepts above you can apply them to your library of choice.

TS

const [getAllProjects, { data: projectsData, loading }] = useLazyQuery<
GetAllProjectsResponse,
GetAllProjectsVars
>(GET_ALL_PROJECTS, { fetchPolicy: "cache-first" })

A demo is available here

To comment please authenticate