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.
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
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(); }
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, }); };
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 ({postLists?.pages.map((page) => page.data.map(post => (); }; export default PostList;)) )}{post.title}
{post.body}
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 ({postLists?.pages.map((page, i) => page.data.map(post => (); }; export default PostList;)) )}{post.title}
{post.body}
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.
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, }); };
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);
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]);
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; }; export default function ToTopBtn({ elemRef }: ToTopBtnProps) { return ( ); }
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 ();
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.
Disclaimer: All resources provided are partly from the Internet. If there is any infringement of your copyright or other rights and interests, please explain the detailed reasons and provide proof of copyright or rights and interests and then send it to the email: [email protected] We will handle it for you as soon as possible.
Copyright© 2022 湘ICP备2022001581号-3