DEV Community

Cover image for Building a Feed Page Using React Query
Mohamed Hammi
Mohamed Hammi

Posted on

Building a Feed Page Using React Query

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
Enter fullscreen mode Exit fullscreen mode

And, we will install the required dependencies, axios and react-query:

npm install axios @tanstack/react-query@4
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Once the server is set up, we will run it using:

npx json-server --watch db.json
Enter fullscreen mode Exit fullscreen mode

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();
  }
Enter fullscreen mode Exit fullscreen mode

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,
  });
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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,
  });
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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] });
    },
  });
};
Enter fullscreen mode Exit fullscreen mode
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>
  );
Enter fullscreen mode Exit fullscreen mode

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)