Infinite Scrolling with React Query: A Step-by-Step Guide

Muhaymin Bin Mehmood

Muhaymin Bin Mehmood

· 5 min read
Infinite Scrolling with React Query: A Step-by-Step Guide Banner Image
Infinite Scrolling with React Query: A Step-by-Step Guide Banner Image

In this blog post, we'll delve into implementing infinite scrolling functionality using React Query, a powerful library for managing asynchronous data fetching in React applications. Here, we'll build upon the provided code and offer insights into each line, along with suggestions for further optimization.

Understanding the Code:

1. Imports:

import { useState } from 'react';
import { useQuery } from 'react-query';

Explanation:

  • useState: This React Hook is used to manage the component's state, in this case, to store the fetched posts.
  • useQuery: This Hook from React Query facilitates fetching data asynchronously and managing its state (loading, error, success).

2. fetchPosts Function:

const fetchPosts = async (page = 1) => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${page}`);
  return response.json();
};

Explanation:

  • This function fetches data from the JSONPlaceholder API, taking an optional page parameter for pagination.
  • It makes a fetch request to the API endpoint, including the page number in the query string.
  • The response is converted to JSON format using response.json().

3. Generate Random color

const getRandomColor = () => {
  const letters = '0123456789ABCDEF';
  let color = '#';
  for (let i = 0; i < 6; i++) {
    color += letters[Math.floor(Math.random() * 16)];
  }
  return color;
};

Explanation:

Function Declaration:

  • const getRandomColor = () => { ... }: This line defines a constant function named getRandomColor that uses an arrow function syntax (=>).

Character Pool:

  • const letters = '0123456789ABCDEF';: This line creates a constant variable named letters and assigns a string containing all valid hexadecimal characters (digits 0-9 and letters A-F).

Color String Initialization:

  • let color = '#';: This line declares a variable named color and initializes it with the '#' character, which is the prefix for hexadecimal color codes.

Loop for Generating Random Characters:

  • for (let i = 0; i < 6; i++) { ... }: This loop iterates six times (i < 6).
    • Inside the loop:
      • Math.floor(Math.random() * 16): This expression generates a random integer between 0 and 15 (inclusive) using Math.random() and Math.floor().
      • letters[ ... ]: This accesses a random character from the letters string based on the generated index.
      • color += ...: The randomly chosen character is appended to the color string using the concatenation operator (+=).

Returning the Color Code:

  • return color;: After the loop completes, the final color string, which is a valid hexadecimal color code, is returned from the function.

4. App Component:

const App = () => {
  const [data, setData] = useState([]);
  const { error, isLoading, isFetching, isPreviousData } = useQuery('posts', () => fetchPosts(), {
    staleTime: 30000, // 30 seconds
    keepPreviousData: true,
    onSuccess: (fetchedData) => {
      setData(fetchedData);
    },
  });

  const fetchMorePosts = async () => {
    const nextPage = Math.ceil(data.length / 10) + 1;
    const newData = await fetchPosts(nextPage);
    setData((prevData) => [...prevData, ...newData]);
  };

Explanation:

  • State and useQuery Hook:
    • The component maintains state for the fetched data (data) using useState.
    • The useQuery Hook from React Query fetches data with the key 'posts'.
      • The query function calls fetchPosts.
      • staleTime: 30000 keeps the data fresh for 30 seconds after the last successful fetch.
      • keepPreviousData: true preserves previous data during subsequent fetches.
      • The onSuccess callback updates the data state with the fetched data.
  • fetchMorePosts Function:
    • This function handles fetching new posts when the user reaches the bottom of the list.
    • It calculates the next page number based on the current data length (assuming 10 posts per page).
    • It calls fetchPosts with the next page number.
    • The fetched data is appended to the existing data using the callback function in setData.

5. Rendering and User Interaction:


  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div style={{ width: '90%', margin: 'auto' }}>
      <h1>Infinite Scrolling with React Query</h1>
      <div>
        {data.map((post, index) => {
          const borderColor = getRandomColor();
          return (
            <div key={post.id} style={{ borderLeft: `3px solid ${borderColor}`, padding: '20px 24px 20px', marginBottom: '30px', background: `linear-gradient(45deg, ${borderColor}0C, transparent)` }}>
              <h3 style={{ margin: '0', lineHeight: 1, }}>{post.title}</h3>
              <p style={{ marginBottom: '0' }}>{post.body}</p>
            </div>
          );
        })}
        <div>
          <button
            onClick={() => {
              fetchMorePosts();
            }}
            disabled={isFetching}
          >
            {isFetching || isPreviousData ? 'Loading more...' : 'Load more'}
          </button>
        </div>
      </div>
    </div>
  );

Explanation:

Conditional Rendering:

  • The component displays a loading message while data is being fetched (isLoading).
  • If an error occurs during data fetching (error is truthy), the component renders an error message displaying the error's message (error.message).
  • The data.map function iterates through the fetched posts, rendering each post within a div element.
  • Each post includes elements for the title (<h2>), body (<p>), and a potential loading indicator.
  • The key condition (key={post.id}) ensures efficient updates in React.
  • The conditional rendering within the map function handles the "Loading more..." message for the last post:
    • It checks two conditions:
      • isFetching: This indicates if new data is being fetched.
      • data.length === index + 1: This identifies if the current post is the last one in the rendered list.
    • If both conditions are true, it displays the "Loading more..." message, indicating that more data is being fetched specifically for the next page.
  • The button triggers the fetchMorePosts function when clicked.
  • It's disabled (disabled={isFetching}) while new data is being fetched to prevent duplicate requests.
  • The button text conditionally changes based on the data fetching state:
    • "Loading more..." shows when data is being fetched (isFetching) or when there's previous data to display (isPreviousData).
    • "Load more" is displayed when both isFetching and isPreviousData are false, indicating no active fetching and no previously displayed data.

Complete Code

import { QueryClient, QueryClientProvider } from 'react-query';

const index = () => (
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);

export default index;
import { useState } from 'react';
import { useQuery } from 'react-query';

const fetchPosts = async (page = 1) => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${page}`);
  return response.json();
};

const getRandomColor = () => {
  const letters = '0123456789ABCDEF';
  let color = '#';
  for (let i = 0; i < 6; i++) {
    color += letters[Math.floor(Math.random() * 16)];
  }
  return color;
};

const App = () => {
  const [data, setData] = useState([]);
  const { error, isLoading, isFetching, isPreviousData } = useQuery('posts', () => fetchPosts(), {
    staleTime: Infinity,
    keepPreviousData: true,
    onSuccess: (fetchedData) => {
      setData(fetchedData);
    }
  });

  const fetchMorePosts = async () => {
    const nextPage = Math.ceil(data.length / 10) + 1;
    const newData = await fetchPosts(nextPage);
    setData((prevData) => [...prevData, ...newData]);
  };

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div style={{ width: '90%', margin: 'auto' }}>
      <h1>Infinite Scrolling with React Query</h1>
      <div>
        {data.map((post, index) => {
          const borderColor = getRandomColor();
          return (
            <div key={post.id} style={{ borderLeft: `3px solid ${borderColor}`, padding: '20px 24px 20px', marginBottom: '30px', background: `linear-gradient(45deg, ${borderColor}0C, transparent)` }}>
              <h3 style={{ margin: '0', lineHeight: 1, }}>{post.title}</h3>
              <p style={{ marginBottom: '0' }}>{post.body}</p>
            </div>
          );
        })}
        <div>
          <button
            onClick={() => {
              fetchMorePosts();
            }}
            disabled={isFetching}
          >
            {isFetching || isPreviousData ? 'Loading more...' : 'Load more'}
          </button>
        </div>
      </div>
    </div>
  );
};

export default App;
Muhaymin Bin Mehmood

About Muhaymin Bin Mehmood

I am a Front-End Developer specializing in Frontend at Dvago.pk.

Copyright © 2024 Mbloging. All rights reserved.