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:
-
Component Structure: Create a functional React component
InfiniteScrollListthat accepts the following props:fetchItems: A function that returns a Promise. This function simulates fetching data from an API. It should accept apagenumber (starting from 1) and alimit(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 }>
- Signature:
ItemComponent: A React functional component that will be used to render each individual item in the list. It should accept anitemofItemTypeas 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.
-
State Management:
- Maintain the current list of loaded
items. - Keep track of the current
pagenumber to fetch. - Manage a loading state to indicate when data is being fetched.
- Track whether there are
hasMoreitems to load.
- Maintain the current list of loaded
-
Scroll Detection:
- Implement logic to detect when the user scrolls near the bottom of the page.
- When the scroll threshold is met and
hasMoreis true and not currently loading, trigger a fetch for the next page of items.
-
Data Fetching:
- Use
useEffectto initially fetch the first page of data when the component mounts. - Fetch subsequent pages based on scroll events.
- Use
-
Rendering:
- Render the
ItemComponentfor each item in theitemsstate. - Display a loading indicator (e.g., "Loading more items...") when
isLoadingis true andhasMoreis true.
- Render the
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 (
hasMoreis 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 (
hasMorebecomes 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
fetchItemsreturnshasMore: falseon the initial fetch or any subsequent fetch. - Rapid Scrolling: The loading state should prevent multiple simultaneous fetches.
- Empty API Response: If
fetchItemsreturns an empty array buthasMoreis true, the component should continue to try fetching. IfhasMoreis 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
InfiniteScrollListcomponent is rendered withfetchItems={mockFetchPosts}andItemComponentset to render aPost. - Expected Output (Visual):
- Initially, the first 10 posts (assuming
itemsPerPagedefaults 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.
- Initially, the first 10 posts (assuming
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:
InfiniteScrollListis rendered withfetchItems={mockFetchPosts},ItemComponent, anditemsPerPage={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
fetchItemsfunction will always return aPromisethat resolves. - The
ItemComponentprop will be a valid React functional component. - The
ItemTypewill 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
useRefto store and access mutable values like the current page number and the container element for scroll detection. - The
window.innerHeightanddocument.documentElement.scrollTopproperties 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
ItemComponentshould be responsible for rendering the visual representation of a singleItemType. - Think about how to handle the
IntersectionObserverAPI as an alternative to direct scroll event listeners for more efficient element visibility detection, though direct scroll events are also acceptable for this challenge.