jgifford82
Posted on June 1, 2023
I'm at the end of the fifth (and final!) phase of my coding boot camp. We previously learned about JavaScript, React, and Ruby on Rails. In this phase, we had to complete a full stack project utilizing React and Ruby on Rails that also implements something new that we hadn't learned in the boot camp. I chose to implement infinite scroll.
In my project, infinite scroll pulls in additional data as a user scrolls down the page. This can be a helpful alternative to fetching all the data up front when there is a large amount of data, which could potentially be slow to load.
It's possible to implement infinite scroll using React's built-in functions, though I chose to implement it using the react-infinite-scroll-component.
Here's how I did it:
1. Set up the backend controller
Note: As I build out projects, I like to set up each feature on the backend first, then the frontend, though you don't necessarily have to do it in this order.
My project is a tea recipe sharing app, and I wanted infinite scroll to pull in 3 teas at a time. Since it's pulling in multiple teas, it's going to route to the index method of the teas controller. Therefore, my routes.rb file needs the routing logic to have the teas resource with index method:
resources :teas, only: [:index]
In the teas controller, I set up the index method to GET 3 teas at a time alphabetically by name regardless of capitalization. The data is broken up into pages containing a specific number of items, 3 teas per page in this example, reducing the amount of data transferred and improving the overall performance. The page number parameter from the request is converted to an integer, and checks if the integer is positive. Otherwise, it defaults to 1 to prevent the database error "OFFSET must not be negative."
The line offset((page - 1) * per_page)
skips the appropriate number of teas based on the current page number and the number of teas per page. Then, limit(per_page)
limits the result to the specified number of teas per page.
def index
page = params[:page].to_i.positive? ? params[:page].to_i : 1
per_page = 3
teas = Tea.order('lower(name)').offset((page - 1) * per_page).limit(per_page)
render json: teas, include: ['category', 'reviews', 'reviews.user']
end
I tested this in Postman by sending a GET request to localhost:3000/teas, which successfully returns JSON displaying the first 3 teas in alphabetical order.
2. Install React infinite scroll component
With the backend taken care of, it's time to switch gears to the frontend. In my project, I navigated to the folder containing my frontend files to install the React infinite scroll component. It's important to install it in the correct folder directory, otherwise it could lead to errors, which I learned the hard way! After navigating to the correct folder, in this case "client," I ran npm i react-infinite-scroll-component
based on the component documentation.
3. Import InfiniteScroll
The component that will utilize infinite scroll needs to import it. In my project, I used it in my TeasList component, so it has this code at the top:
import InfiniteScroll from "react-infinite-scroll-component";
4. Fetch data
When the TeasList component mounts, a useEffect hook fetches teas data as shown below. It's only pulling in the first page of teas since the backend was set up to divide the data into pages. It converts the data to JSON, then populates teas state with that data. I used useContext for teas state to prevent props drilling through multiple components.
useEffect(() => {
fetch("/teas?page=1")
.then((r) => r.json())
.then((data) => setTeas(data));
}, [setTeas]);
At the top of the TeasList component, I imported the necessary hooks for that fetch:
import { useContext, useState, useEffect } from "react";
Teas context is imported as well:
import { TeasContext } from "../context/teas";
Inside the function body of the TeasList component, teas state is destructured from TeasContext so it can be used in the fetch request, as well as the next fetch request in the next step:
const { teas, setTeas } = useContext(TeasContext);
5. Fetch more data
The previous step only pulls in the first page of data when you first navigate to the page. Another function is needed to pull in more data. The fetchMoreTeas function below handles this. The function is triggered when the user reaches the end of the page. It updates teas state with newly fetched data, increments the page state for the next fetch, and sets hasMore to false if there are no more teas to fetch.
const fetchMoreTeas = async () => {
const response = await fetch(`/teas?page=${page + 1}`);
const data = await response.json();
if (data.length > 0) {
setTeas((prevTeas) => [...prevTeas, ...data]);
setPage((prevPage) => prevPage + 1);
} else {
setHasMore(false);
}
};
Since the function updates page
and hasMore
state, both of those are set up within the TeasList component. The page
state keeps track of the current page for fetching teas and is set with page 1 as the default value:
const [page, setPage] = useState(1);
The hasMore
state keeps track of whether there is more data available for infinite scroll and is set as true by default.
const [hasMore, setHasMore] = useState(true);
6. Render InfiniteScroll component
Now that infinite scroll is imported and fetches are set up, we need to render the infinite scroll functionality. The data being displayed needs to be wrapped within <InfiniteScroll> </InfiniteScroll>
. In the example below, teas state is being mapped over to display the tea name and blend, and the ul contains the tea id as a key. That code block is wrapped with InfiniteScroll, which contains props that are necessary for infinite scroll to work. You can find these props in the documentation as well as other props you can use depending on how you want to implement infinite scroll.
- The
dataLength
prop is the length of the data array. If the length is falsy (0, null, or undefined), the OR operator evaluates to 0, ensuring a valid value so the page doesn't crash. - The
next
prop specifies the function that should be called when the user reaches the end of the scrollable content, which is fetchMoreTeas in this case. - The
hasMore
prop is a Boolean indicating if there is more data to load; fetchMoreTeas sets it to false when there is no more data. - The
loader
prop displays a "Loading..." header while loading more data.
return (
<div>
<InfiniteScroll
dataLength={teas.length || 0}
next={fetchMoreTeas}
hasMore={hasMore}
loader={<h4>Loading...</h4>}
>
{teas.map((tea) => (
<ul key={tea.id}>
{tea.name}
{tea.blend}
</ul>
))}
</InfiniteScroll>
</div>
);
};
Hopefully this helps others implement infinite scroll! Happy scrolling!
Posted on June 1, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.