Hone logo
Hone
Problems

Implementing Infinite Scroll in React

Infinite scroll is a common UI pattern that loads more content as the user scrolls down a page, creating a seamless and engaging user experience. This challenge asks you to build a React component that implements infinite scroll to load and display a list of items.

Problem Description

Your task is to create a React functional component named InfiniteScrollList that displays a list of items. This list should dynamically load more items from a simulated API as the user scrolls towards the bottom of the visible content.

Key Requirements:

  1. Component Structure: Create a functional React component InfiniteScrollList that accepts the following props:

    • fetchItems: A function that returns a Promise. This function simulates fetching data from an API. It should accept a page number (starting from 1) and a limit (number of items per page) and resolve with an array of items and a boolean indicating if there are more items to load.
      • Signature: (page: number, limit: number) => Promise<{ items: ItemType[], hasMore: boolean }>
    • ItemComponent: A React functional component that will be used to render each individual item in the list. It should accept an item of ItemType as a prop.
    • ItemType: A TypeScript type defining the structure of each item.
    • itemsPerPage (optional, default 10): A number representing how many items to fetch per page.
  2. State Management:

    • Maintain the current list of loaded items.
    • Keep track of the current page number to fetch.
    • Manage a loading state to indicate when data is being fetched.
    • Track whether there are hasMore items to load.
  3. Scroll Detection:

    • Implement logic to detect when the user scrolls near the bottom of the page.
    • When the scroll threshold is met and hasMore is true and not currently loading, trigger a fetch for the next page of items.
  4. Data Fetching:

    • Use useEffect to initially fetch the first page of data when the component mounts.
    • Fetch subsequent pages based on scroll events.
  5. Rendering:

    • Render the ItemComponent for each item in the items state.
    • Display a loading indicator (e.g., "Loading more items...") when isLoading is true and hasMore is true.

Expected Behavior:

  • Upon initial render, the component fetches and displays the first page of items.
  • As the user scrolls down, the component detects the scroll position.
  • When the user reaches a certain threshold (e.g., 80% of the way down the scrollable content), and if there are more items to load (hasMore is true) and no fetch is currently in progress, the component fetches the next page of data.
  • The newly fetched items are appended to the existing list of items.
  • A loading indicator is shown while new data is being fetched.
  • Once all items are loaded (hasMore becomes false), the loading indicator should disappear, and no further fetches should be attempted.

Edge Cases:

  • Initial Load: Ensure the first page is fetched correctly on mount.
  • No More Items: Handle the scenario where fetchItems returns hasMore: false on the initial fetch or any subsequent fetch.
  • Rapid Scrolling: The loading state should prevent multiple simultaneous fetches.
  • Empty API Response: If fetchItems returns an empty array but hasMore is true, the component should continue to try fetching. If hasMore is false and the array is empty, it should stop loading.
  • Error Handling: (Optional for this challenge, but good to consider) How would you handle API errors?

Examples

Let's define a sample ItemType and a mock fetchItems function for demonstration.

// Assume this type is defined elsewhere or provided
interface Post {
  id: number;
  title: string;
  body: string;
}

// Mock data and fetch function for demonstration
const mockPosts: Post[] = Array.from({ length: 100 }, (_, i) => ({
  id: i + 1,
  title: `Post Title ${i + 1}`,
  body: `This is the body of post number ${i + 1}. It contains some sample text.`,
}));

const mockFetchPosts = async (page: number, limit: number): Promise<{ items: Post[], hasMore: boolean }> => {
  console.log(`Fetching page ${page} with limit ${limit}`);
  await new Promise(resolve => setTimeout(resolve, 500)); // Simulate network delay

  const startIndex = (page - 1) * limit;
  const endIndex = startIndex + limit;
  const items = mockPosts.slice(startIndex, endIndex);
  const hasMore = endIndex < mockPosts.length;

  return { items, hasMore };
};

Example 1: Initial Load and Scroll to Trigger Next Page

  • Input: The InfiniteScrollList component is rendered with fetchItems={mockFetchPosts} and ItemComponent set to render a Post.
  • Expected Output (Visual):
    • Initially, the first 10 posts (assuming itemsPerPage defaults to 10) are displayed.
    • A loading indicator might appear briefly after the first set is rendered.
    • As the user scrolls down, when they are about 80% towards the bottom of the content, the "Loading more items..." indicator appears.
    • After a short delay (simulated by setTimeout), the next 10 posts are appended to the list, and the loading indicator disappears. This process repeats.

Example 2: Reaching the End of Data

  • Input: The user has scrolled enough to load all 100 posts. The last fetch returned hasMore: false.
  • Expected Output (Visual):
    • All 100 posts are displayed.
    • The "Loading more items..." indicator does not appear again.
    • No further attempts to fetch data are made.

Example 3: Handling itemsPerPage prop

  • Input: InfiniteScrollList is rendered with fetchItems={mockFetchPosts}, ItemComponent, and itemsPerPage={20}.
  • Expected Output (Visual):
    • Initially, 20 posts are displayed.
    • Subsequent fetches will request 20 items at a time. The scroll trigger behavior remains the same.

Constraints

  • The fetchItems function will always return a Promise that resolves.
  • The ItemComponent prop will be a valid React functional component.
  • The ItemType will be a valid TypeScript type.
  • The scroll detection threshold should be configurable or a reasonable fixed value (e.g., 80% of the scrollable height).
  • The component should be performant enough to handle lists of hundreds or even thousands of items without significant lag.

Notes

  • Consider using useRef to store and access mutable values like the current page number and the container element for scroll detection.
  • The window.innerHeight and document.documentElement.scrollTop properties can be useful for calculating scroll position.
  • Be mindful of throttling or debouncing scroll event handlers if performance becomes an issue, though for this challenge, a direct implementation is expected.
  • The ItemComponent should be responsible for rendering the visual representation of a single ItemType.
  • Think about how to handle the IntersectionObserver API as an alternative to direct scroll event listeners for more efficient element visibility detection, though direct scroll events are also acceptable for this challenge.
Loading editor...
typescript