In this task, you will implement an infinite scroll feature in your application. Infinite scrolling is a popular UI pattern that allows users to load more content as they scroll down the page. This feature is commonly used in social media feeds, search results, and other applications where content is paginated.
Step 1: Update the API function
First, let’s modify the fetchPosts function in web/src/data/api.ts to accept pagination parameters:
This update allows us to fetch posts with pagination. The function now returns both the posts and the total number of posts, which we’ll use to determine if there are more posts to load.
Step 2: Update the store
We need to modify our store to handle the concept of pages and the ability to append new posts instead of replacing them entirely. Let’s update web/src/lib/store.ts:
Here’s what we’ve changed:
We’ve added two new atoms: $currentPage to keep track of the current page, and $hasMorePosts to indicate if there are more posts to load.
We’ve added an appendPosts function to add new posts to the existing list instead of replacing them.
We’ve added incrementPage and setHasMorePosts functions to update these new state values.
The rest of the store remains the same for now.
Step 3: Update useQueryPosts hook
We need to modify our useQueryPosts hook to handle pagination. Let’s update the web/src/hooks/use-query-posts.tsx file:
Here’s what we’ve changed:
The loadPosts function now:
Accepts a page parameter to fetch posts for a specific page.
Sets the hasMorePosts state based on the total number of posts.
Appends new posts to the existing list when loading more.
We’re returning loadPosts along with the posts, which we’ll use in our components.
This setup allows us to load more posts as needed, which is perfect for infinite scrolling.
Step 4: Create an InfiniteScroll Component
Let’s start with the basics and build up our InfiniteScroll component step by step, explaining each concept as we go along.
Let’s begin with your initial component in web/src/components/post/infinite-scroll.tsx:
This is our starting point. Right now, it’s just a wrapper component that renders its children. We will turn this into a functional infinite scroll component later.
Step 5: Incorporate InfiniteScroll Component
Next, we’ll update the Posts component to use the InfiniteScroll component:
Notice we wrapped the posts with the InfiniteScroll component. The array of posts will be passed as children to the InfiniteScroll component.
Step 6: Add Load More Button
Let’s add a “Load More” button to the InfiniteScroll component. We’ll also pass a loadMore function as a prop to handle loading more posts.
In the code above, we’ve added a loadMore function prop to the InfiniteScroll component. This function will be called when the “Load More” button is clicked. This button will be displayed at the end of the list of posts.
Step 7: Update Posts Component
Let’s update the Posts component to handle loading more posts when the “Load More” button is clicked:
Here’s what we’ve changed:
We’ve imported two new atoms: $currentPage and $hasMorePosts that keep track of the current page and whether there are more posts to load.
We’ve added a loadMorePosts function to handle loading more posts when the “Load More” button is clicked. This function increments the current page and calls the loadPosts function with the new page number. It also checks if there are more posts to load before making the API call.
We’ve passed the loadMorePosts function to the InfiniteScroll component as a prop.
Test the Load More Functionality
Open your browser and navigate to the home page. You should see the list of posts with a “Load More” button at the end. Clicking the button should load more posts, and the button should disappear when there are no more posts to load.
Step 8: Introducing IntersectionObserver
We don’t want to have user click the “Load More” button every time they reach the end of the list. Instead, we want to trigger the loading of more posts automatically when the user scrolls to the end of the list. We can achieve this using the Intersection Observer API.
Update the InfiniteScroll component to use the Intersection Observer API:
In the code above:
We’ve added an useEffect hook to create an IntersectionObserver instance.
We’ve defined an empty options object and a callback function that logs the entries to the console.
We’ve created an IntersectionObserver instance with the callback function and options.
We’ve added the id of “trigger” to the “Load More” button.
We get the element with the id “trigger” and observe it using the IntersectionObserver instance.
Open your browser and navigate to the home page. You should see the list of posts with a “Load More” button at the end. If you open the console, you should see the entries logged. Notice when the “Load More” button is in view, the related entry’s isIntersecting property is true. If the button is not in view, the property is false.
Step 9: Load More Posts with Intersection Observer
Let’s update the IntersectionObserver callback to load more posts when the “Load More” button is in view:
If you run the application now, you will see we are loading more posts when the button is visible on the screen. However, there will be some errors in the console. This is because the IntersectionObserver is not being disconnected when the component is unmounted. We will fix this in the next step.
Step 10: Clean Up Intersection Observer
When more posts are loaded, the Posts component is re-rendered, and the IntersectionObserver is re-initialized. This in turn causes the IntersectionObserver to observe the “Load More” button multiple times, leading to multiple calls to the loadMore function. To fix this, we need to disconnect the IntersectionObserver when the component is unmounted.
The mechanism to clean up the IntersectionObserver is to return a cleanup function from the useEffect hook. This function will be called when the component is unmounted. Let’s update the InfiniteScroll component to disconnect the IntersectionObserver when the component is unmounted:
This update will ensure that the IntersectionObserver is disconnected when the component is unmounted. It will reduce the number of errors in the console. However, if you scroll down too fast, you might still see some errors. This is because we attempt to fetch the posts while the previous fetch is still in progress. We will fix this in the next step.
Step 11: Handle Loading State
Let’s update the useQueryPosts hook to handle the loading state. If we know that a fetch operation is already in progress, we can refrain from making another fetch request. This will prevent multiple fetch requests from being made when the user scrolls down too fast.
In the code above, we’ve added a new state variable isLoading to keep track of the loading state. We set this state to true before making the fetch request and set it to false after the fetch operation is complete. We also return the isLoading state from the hook.
Update the Posts Component
Next, update the Posts component to load more posts only when the previous fetch operation is complete:
In the code above, we get the isLoading state from the useQueryPosts hook. We use this state to prevent loading more posts when a fetch operation is already in progress.
Test the Infinite Scroll Functionality
Open the browser and navigate to the home page. Scroll down to the end of the list of posts. You should see more posts being loaded automatically as you scroll. The “Load More” button should not be visible until all posts are loaded.
There should be no console errors when loading more posts with the Intersection Observer.
Step 12
We have a working infinite scroll feature that loads more posts automatically when the user scrolls to the end of the list. We will now refactor this into a more efficient and reusable component.
Initial IntersectionObserver Options
In the InfiniteScroll component, where you define the const options = {};, let’s add some initial options to improve the performance of the IntersectionObserver.
Here is the breakdown of the options:
root: The element that is used as the viewport for checking visibility of the target. The default is the browser viewport. If you want to use a different element, you can set it here. We are using the default value null.
rootMargin: Margin around the root. The default is “0px”. You can set it to a positive or negative value. We are setting it to “20px”. This means that the target will be visible when it is 20px away from the root element. This will help us to load the posts a bit earlier.
threshold: The percentage of the target’s visibility the observer’s callback should be executed. The default is 0.0. This means that the callback will be executed as soon as the target is visible. If you set this value to 1.0, the callback will be executed only when the target is fully visible.
Introducing useRef
Let’s update the InfiniteScroll component to use the useRef hook to get a reference to the “Load More” button element:
In this update, instead of using document.getElementById("trigger"), we are using a useRef hook to get the reference to the button element. This is a better approach because it is more efficient and less error-prone.
Understanding useRef
useRef is a hook that returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.
We use useRef to get a reference to a DOM element or a value that persists between renders. In this case, we are using it to get a reference to the button element.
Replace Button with div Element
Let’s get rid of the button and use a div element instead:
In this update, we’ve replaced the Button element with a div element. We’ve also updated the triggerRef to use HTMLDivElement instead of HTMLButtonElement. This update make sense as we don’t need a button element for the “Load More” functionality.
Add Loading Spinner
Let’s pass more props to InfiniteScroll component to handle loading state and add a loading spinner:
Update the Posts component
Let’s update the Posts component to pass the hasMore and isLoading props to the InfiniteScroll component:
Notice that we’ve passed the hasMorePosts and isLoading states to the InfiniteScroll component. This will allow the InfiniteScroll component to show a loading spinner when more posts are being loaded. Moreover, we’ve removed the condition to check if isLoading in the loadMorePosts function. This is because the InfiniteScroll component will handle this for us.
Test the Loading Spinner
Open the browser and navigate to the home page. Then open the developer tools and throttle the network to simulate a slow connection. Scroll down to the end of the list of posts. You should see a loading spinner when more posts are being loaded. The spinner should disappear when all posts are loaded.
Make the InfiniteScroll Component More Reusable
Let’s make a few improvements to the InfiniteScroll component so that it is more reusable and flexible. We’ll add some additional props to customize the loading spinner and the trigger element.
In the updated InfiniteScroll component, we’ve added two new props:
observerOptions: This prop allows you to customize the IntersectionObserver options. You can pass any valid IntersectionObserverInit object to this prop. By default, we are using the same options as before.
loader: This prop allows you to customize the loading spinner. You can pass any React element to this prop. By default, we are using the same loading spinner as before.
Introducing useCallback
Let’s make more improvements to the InfiniteScroll component by moving the IntersectionObserver callback to a separate function.
In the code above, we moved the IntersectionObserver callback to a separate function called observerCallback. We also memoized the function using the useCallback hook. This will prevent the function from being recreated on every render. It will only be recreated when the dependencies (loadMore, hasMore, and isLoading) change.
Understanding useCallback
useCallback is a hook that returns a memoized callback function. It is useful when you want to pass a function to a child component that relies on some values from the parent component.
In this case, we are using useCallback to memoize the observerCallback function. This will prevent the function from being recreated on every render. It will only be recreated when the dependencies (loadMore, hasMore, and isLoading) change.
Simplify the IntersectionObserver Callback
Since we only add one element to the IntersectionObserver, we can simplify the code by running the callback on the first entry.
In the updated code, we are getting the first entry from the entries array and checking if it is intersecting. If it is, we call the loadMore function. This simplifies the code and makes it easier to understand.
Step 13
Move the InfinityScroll component to web/src/components/shared directory so that it can be reused in other parts of the application.
Make sure the import path is updated in the Posts component that uses the InfiniteScroll component.
Now, you can use the InfiniteScroll component in other parts of the application to implement infinite scrolling. For example, you can use it for loading more comments in a comment section or loading more search results in a search page.
Conclusion
Congratulations! You have successfully implemented an infinite scroll feature in your application using the Intersection Observer API. You have learned how to load more posts automatically when the user scrolls to the end of the list. You have also learned how to handle loading state and display a loading spinner while more posts are being loaded.