The Goal
In this article, we will explore how to build a feed page using React Query!
Here’s what we will be creating :
This article won’t cover every step and detail involved in building the app.
Instead, we will focus on the key features, specifically the "infinite scroll" and "scroll-to-top" functionalities.
If you are interested in consulting the whole implementation, you can find the full codebase in this GitHub repository.
Setting Up the Application
First, we will create our React application using Vite with the following command:
npm create vite@latest feed-page-rq -- --template react-ts
And, we will install the required dependencies, axios and react-query:
npm install axios @tanstack/react-query@4
We also need to mock a RESTful server, so we will use json-server, which allows us to simulate a backend by providing fake API endpoints for our React app.
We will be working with a post
entity that includes the following attributes:
{
"id": "1",
"title": "Sample Post Title",
"body": "This is a sample post body",
"userId": "2",
"createdAt": 1728334799169 // date timestamp
}
Once the server is set up, we will run it using:
npx json-server --watch db.json
Implementing "Infinite Scroll"
The "Infinite Scroll" feature's mechanism is straightforward:
When the user scrolls through the list of posts and approaches the bottom of the container, React Query will look for the next batch of posts. This process repeats until there are no more posts to load.
We verify whether the user is near the bottom by adding the current scroll position (scrollTop
) to the visible screen height (clientHeight
) and comparing this sum with the total height of the container (scrollHeight
).
If the sum is greater than or equal to the total container height, we ask React Query to fetch the next page.
const { scrollTop, scrollHeight, clientHeight } = elemRef.current;
if(scrollTop + clientHeight >= scrollHeight) {
fetchNextPage();
}
Step 1: Configure useInfiniteQuery
First, we will create a custom hook to wrap React Query’s useInfiniteQuery
.
Within the custom hook, we configure the query to fetch posts page by page, specifying the initial page number and the function that retrieves the next pages:
import { QueryFunctionContext, useInfiniteQuery } from "@tanstack/react-query";
import axios from "axios";
const URL = "http://localhost:3000";
const POSTS = "posts";
export const fetchInfinitePosts = async ({
pageParam,
}: QueryFunctionContext) => {
const result = await axios.get(
`${URL}/${POSTS}?_sort=-createdAt&_page=${pageParam}&_per_page=10`,
);
return result.data;
};
export const useInfinitePosts = () => {
return useInfiniteQuery({
queryKey: [POSTS],
queryFn: fetchInfinitePosts,
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.next,
});
};
Step 2: Display posts
in PostList
Now, we will use the custom hook in our component to display the list of posts.
To do this, we first loop through the pages and then iterate over the posts within each page to render them.
import { useInfinitePosts } from './hooks/useInfinitePosts';
const PostList = () => {
const { data: postLists } = useInfinitePosts();
return (
<div style={{ height: '500px', overflowY: 'scroll' }}>
{postLists?.pages.map((page) =>
page.data.map(post => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
))
)}
</div>
);
};
export default PostList;
Step 3: Implement the Infinite Scroll Behaviour
To implement the infinite scroll behaviour, we need to add a scroll event listener to the container where posts are displayed. This event listener triggers the onScroll
function, which checks if the user is near the bottom of the container and, if so, calls fetchNextPage
to load more content.
import React, { useRef, useEffect } from 'react';
import { useInfinitePosts } from './hooks/useInfinitePosts';
const PostList = () => {
const { data: postLists, fetchNextPage } = useInfinitePosts();
const elemRef = useRef(null);
const onScroll = useCallback(() => {
if (elemRef.current) {
const { scrollTop, scrollHeight, clientHeight } = elemRef.current;
const isNearBottom = scrollTop + clientHeight >= scrollHeight;
if(isNearBottom) {
fetchNextPage();
}
}
}, [fetchNextPage]);
useEffect(() => {
const innerElement = elemRef.current;
if (innerElement) {
innerElement.addEventListener("scroll", onScroll);
return () => {
innerElement.removeEventListener("scroll", onScroll);
};
}
}, [onScroll]);
return (
<div ref={elemRef} style={{ height: '500px', overflowY: 'scroll' }}>
{postLists?.pages.map((page, i) =>
page.data.map(post => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
))
)}
</div>
);
};
export default PostList;
Implementing "Scroll to Top"
Next, we will create a "Scroll to Top" button that appears when a new post is added. This button lets the user quickly return to the top to see the latest update.
Since posts are sorted by creation date, any newly added post will appear at the top of the list.
Our feature's logic will be built on top of this premise.
Step 1: Create a Query for prevNewestPost
We start by creating a new query to fetch and cache the latest created post. We will call this post prevNewestPost
.
We want prevNewestPost
to stay a few steps behind, or at most, match the first post of the list. So, we will manually control its refetch.
We will achieve this by setting enabled: false
in the query options.
export const fetchNewestPost = async () => {
const result = await axios.get(`${URL}/${POSTS}?_sort=-createdAt`);
return result.data[0];
};
export const useNewestPost = () => {
return useQuery({
queryKey: [POSTS, "newest"],
queryFn: () => fetchNewestPost(),
enabled: false,
});
};
Step 2: Compare prevNewestPost
with the First Post
With React Query, the post list are updated automatically on specific events. (Here's the documentation link for a complete list of these events.)
We will use this updated list to determine when to display the 'Scroll To Top' button by comparing prevNewestPost
with the first post.
If they are different, this indicates that a new post has been added, so the 'Scroll To Top' button will be shown.
setIsShowButton(postLists?.pages[0].data[0].id !== prevNewestPost?.id);
Step 3: Hide Button When Cursor at the Top
We should not show the "Scroll To Top" button when the user is at the top of the Post List Container.
So, when the user reaches the top, we need to resync the prevNewestPost
with the current latest post by triggering a query refetch
.
const { data: prevNewestPost, refetch } = useNewestPost();
const [isShowButton, setIsShowButton] = useState(false);
useEffect(() => {
if (!isNearTop) {
setIsShowButton(postLists?.pages[0].data[0].id !== prevNewestPost?.id);
} else {
setIsShowButton(false);
refetch();
}
}, [postLists, prevNewestPost, isNearTop]);
Step 4: Create the Scroll To Top Button
Clicking the ToTopBtn
button will scroll to the top of the list, triggering the existing logic to hide the button and refetch data to sync prevNewestPost
with the first post of the list.
import { RefObject } from "react";
type ToTopBtnProps = {
elemRef: RefObject<HTMLElement>;
};
export default function ToTopBtn({ elemRef }: ToTopBtnProps) {
return (
<div>
<button
onClick={() => {
elemRef.current?.scrollTo({ top: 0, behavior: "smooth" });
}}
>
<p> ↑ New Post</p>
</button>
</div>
);
}
Step 5: Test by Adding New Posts
To test our "Scroll to Top" button functionality, we need to add new posts to the feed.
For this, we will use useMutation
from React Query to add a new post to the server and revalidate our cached postList
after each mutation.
We will set up a mutation that allows us to create a new post with random data whenever the user clicks a button.
export const savePost = async (post: NewPostData) =>
axios.post(`${URL}/${POSTS}`, post);
export const useAddPost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: savePost,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [POSTS] });
},
});
};
export default function AddNewPostBtn() {
const mutation = useAddPost();
return (
<div>
<button
title="Add a new post"
onClick={() => {
const index = Math.floor(Math.random() * postTitles.length);
mutation.mutate({
title: postTitles[index], // Array that contains random post titles
body: postBodies[index], // Array that contains random post bodies
userId: Math.floor(Math.random() * 100).toString(),
createdAt: new Date().getTime(),
});
}}
>
<p>+</p>
</button>
</div>
);
Conclusion
In this tutorial, we explored the power of React Query through a real use case, highlighting its ability to help developers build dynamic interfaces that enhance user experience.
Top comments (0)