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 ( <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;
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;
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<HTMLElement>; }; export default function ToTopBtn({ elemRef }: ToTopBtnProps) { return ( <div> <button onClick={() => { elemRef.current?.scrollTo({ top: 0, behavior: "smooth" }); }} > <p> ↑ New Post</p> </button> </div> ); }
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> );
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.
Haftungsausschluss: Alle bereitgestellten Ressourcen stammen teilweise aus dem Internet. Wenn eine Verletzung Ihres Urheberrechts oder anderer Rechte und Interessen vorliegt, erläutern Sie bitte die detaillierten Gründe und legen Sie einen Nachweis des Urheberrechts oder Ihrer Rechte und Interessen vor und senden Sie ihn dann an die E-Mail-Adresse: [email protected] Wir werden die Angelegenheit so schnell wie möglich für Sie erledigen.
Copyright© 2022 湘ICP备2022001581号-3