This time we are going to implement an infinite scroll using React JS.
An application that implements infinite scroll consists of a layout that allows users to keep consuming a certain amount of information without any pause, as the content is automatically loaded as the user scrolls.
🚨 Note: This post requires you to know the basics of React with TypeScript (basic hooks).
Any kind of feedback is welcome, thank you and I hope you enjoy the article.🤗
Table of contents.
📌 Technologies to be used.
📌 Creating the project.
📌 First steps.
📌 Making the API request.
📌 Showing the cards.
📌 Making the infinite scroll.
📌 Refactoring.
📌 Conclusion.
🎈 Technologies to be used.
- ▶️ React JS (version 18)
- ▶️ Vite JS
- ▶️ TypeScript
- ▶️ React Query
- ▶️ Rick and Morty API
- ▶️ CSS vanilla (You can find the styles in the repository at the end of this post)
🎈 Creating the project.
We will name the project: infinite-scroll
(optional, you can name it whatever you like).
npm init vite@latest
We create the project with Vite JS and select React with TypeScript.
Then we run the following command to navigate to the directory just created.
cd infinite-scroll
Then we install the dependencies.
npm install
Then we open the project in a code editor (in my case VS code).
code .
🎈 First steps.
First in the src/App.tsx
file we will delete the content and add a title.
const App = () => {
return (
<div>
<h1 className="title">React Infinite Scroll</h1>
</div>
)
}
export default App
Next, we are going to create two components that we are going to use later. We create the folder src/components
and inside we create the following files:
- Loading.tsx
This file will contain the following:
export const Loading = () => {
return (
<div className="container-loading">
<div className="spinner"></div>
<span>Loading more characters...</span>
</div>
)
}
It will be used to show a spinner when a new request is made to the API.
- Card.tsx
This file will contain the following:
import { Result } from '../interface';
interface Props {
character: Result
}
export const Card = ({ character }: Props) => {
return (
<div className='card'>
<img src={character.image} alt={character.name} width={50} loading='lazy' />
<p>{character.name}</p>
</div>
)
}
This is the card that will show the Rick and Morty API character.
In the src/interfaces
folder we create an index.ts file and add the following interfaces.
export interface ResponseAPI {
info: Info;
results: Result[];
}
export interface Info {
count: number;
pages: number;
next: string;
prev: string;
}
export interface Result {
id: number;
name: string;
image: string;
}
🚨 Note: The Result interface actually has more properties but in this case I will only use the ones I have defined.
🎈 Making the API request.
In this case we will use the React Query library that will allow us to perform the requests in a better way (and also has other features such as cache management).
- Install the dependency ```
npm i @tanstack/react-query
And then in the `src/main.tsx` file we are going to do the following:
We are going to enclose our **App** component inside the **QueryClientProvider** and send it the client which is just a new instance of **QueryClient**.
```tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
const queryClient = new QueryClient()
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
)
Now in the src/App.tsx
file, we are going to use a special React Query hook called useInfiniteQuery.
const App = () => {
useInfiniteQuery()
return (
<div>
<h1 className="title">React Infinite Scroll</h1>
</div>
)
}
export default App
The useInfiniteQuery hook needs several parameters:
1 - queryKey: an array of strings or nested objects, which is used as a key to manage cache storage.
2 - queryFn: a function that returns a promise, the promise must be resolved or throw an error.
3 - options: within the options we need one called getNextPageParam which is a function that returns the information for the next query to the API.
The first parameter is the queryKey in this case we place an array with the word 'characters'.
const App = () => {
useInfiniteQuery(
['characters']
)
return (
<div>
<h1 className="title">React Infinite Scroll</h1>
</div>
)
}
export default App
The second parameter is the queryFn in this case we place an array with the word 'characters'.
First we pass it a function
const App = () => {
useInfiniteQuery(
['characters'],
() => {}
)
return (
<div>
<h1 className="title">React Infinite Scroll</h1>
</div>
)
}
export default App
This function must return a resolved promise.
For this, outside the component we create a function that will receive as parameter the page to fetch and will return a promise of type ResponseAPI.
import { ResponseAPI } from "./interface"
const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())
const App = () => {
useInfiniteQuery(
['characters'],
() => fetcher()
)
return (
<div>
<h1 className="title">React Infinite Scroll</h1>
</div>
)
}
export default App
The queryFn receives several parameters, among them the pageParam that by default will be undefined and then number, so if a value does not exist, we will equal it to 1, and this property we pass it to the function fetcher.
import { ResponseAPI } from "./interface"
const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())
const App = () => {
useInfiniteQuery(
['characters'],
({ pageParam = 1 }) => fetcher(pageParam),
)
return (
<div>
<h1 className="title">React Infinite Scroll</h1>
</div>
)
}
export default App
Now the last parameter is the options, which is an object, which we will use the getNextPageParam property.
import { ResponseAPI } from "./interface"
const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())
const App = () => {
useInfiniteQuery(
['characters'],
({ pageParam = 1 }) => fetcher(pageParam),
{
getNextPageParam: () => {}
}
)
return (
<div>
<h1 className="title">React Infinite Scroll</h1>
</div>
)
}
export default App
The function getNextPageParam receives two parameters but we will only use the first one that is the last page received (that is to say the last answer that the API gave us).
Inside the function, as the API of Rick and Morty does not come with the next page (rather it comes with the url for the next page) we will have to do the following:
1 - We will obtain the previous page.
The API response comes the info property that contains the prev property, we evaluate if it exists (because in the first call the prev property is null).
- If it does not exist then it is page 0.
- If it exists then we get that string we separate it and get the number.
const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
2 - We will get the current page.
We just add the previous page plus 1.
const currentPage = previousPage + 1;
3 - We will evaluate if there are more pages.
We evaluate if the current page is equal to the total number of pages.
If it is true, then we return false so that it does not make another request.
If false, then we return the next page, which is the result of the sum of the current page plus 1.
if ( currentPage === lastPage.info.pages) return false;
return currentPage + 1;
And this is how the hook would look like.
import { ResponseAPI } from "./interface"
const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())
const App = () => {
useInfiniteQuery(
['characters'],
({ pageParam = 1 }) => fetcher(pageParam),
{
getNextPageParam: (lastPage: ResponseAPI) => {
const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
const currentPage = previousPage + 1;
if (currentPage === lastPage.info.pages) return false;
return currentPage + 1;
}
}
)
return (
<div>
<h1 className="title">React Infinite Scroll</h1>
</div>
)
}
export default App
The hook useInfiniteQuery gives us certain values and functions of which we will use the following.
-
data: an object that contains the API query.
- Inside this property there is another one called pages which is an array containing the obtained pages, from here we will get the API data.
error: An error message caused if the API request fails.
fetchNextPage: function that allows to make a new request to the next API page.
status: a string containing the values "error" | "loading" | "success" indicating the status of the request.
hasNextPage: a boolean value that is true if the getNextPageParam function returns a value that is not undefined.
import { ResponseAPI } from "./interface"
const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())
const App = () => {
const { data, error, fetchNextPage, status, hasNextPage } = useInfiniteQuery(
['characters'],
({ pageParam = 1 }) => fetcher(pageParam),
{
getNextPageParam: (lastPage: ResponseAPI) => {
const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
const currentPage = previousPage + 1;
if (currentPage === lastPage.info.pages) return false;
return currentPage + 1;
}
}
)
return (
<div>
<h1 className="title">React Infinite Scroll</h1>
</div>
)
}
export default App
🎈 Showing the cards.
Now we can show the results thanks to the fact that we already have access to the data.
We create a div and inside we are going to make an iteration on the data property accessing to the page property that is an array that for the moment we will access to the first position and to the results.
In addition we evaluate the status and if it is loading we show the component Loading.tsx but if it is in error, we place the error message.
import { ResponseAPI } from "./interface"
const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())
const App = () => {
const { data, error, fetchNextPage, status, hasNextPage } = useInfiniteQuery(
['characters'],
({ pageParam = 1 }) => fetcher(pageParam),
{
getNextPageParam: (lastPage: ResponseAPI) => {
const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
const currentPage = previousPage + 1;
if (currentPage === lastPage.info.pages) return false;
return currentPage + 1;
}
}
)
if (status === 'loading') return <Loading />
if (status === 'error') return <h4>Ups!, {`${error}` as string}</h4>
return (
<div>
<h1 className="title">React Infinite Scroll</h1>
<div className="grid-container">
{
data?.pages[0].results.map(character => (
<Card key={character.id} character={character} />
))
}
</div>
</div>
)
}
export default App
This only shows the first page, next we will implement infinite scroll.
🎈 Making the infinite scroll.
To do this we are going to use a popular library called react-infinite-scroll-component.
We install the dependency.
npm i react-infinite-scroll-component
First we need the InfiniteScroll component.
<InfiniteScroll/>
This component will receive several properties
dataLength: The number of elements, in a moment we will put the value since we need to calculate it.
next: a function that will be triggered when the end of the page is reached when scrolling. Here we will call the function that useInfiniteQuery offers us, fetchNextPage.
hasMore: boolean property that indicates if there are more elements. Here we will call the property offered by useInfiniteQuery, hasNextPage, and we convert it to boolean with !! because by default it is undefined.
loader: JSX component that will be used to display a loading message while the request is being made. Here we will call the Loading.tsx component.
<InfiniteScroll
dataLength={}
next={() => fetchNextPage()}
hasMore={!!hasNextPage}
loader={<Loading />}
/>
Now, the property dataLength we could but this would only show the next page without accumulating the previous results, so we must do the following:
We will create a stored variable that will change every time the data property of useInfiniteQuery changes.
This characters variable must return a new ResponseAPI but the results property must accumulate the previous and current characters. And the info property will be that of the current page.
const characters = useMemo(() => data?.pages.reduce((prev, page) => {
return {
info: page.info,
results: [...prev.results, ...page.results]
}
}), [data])
Now we pass this constant to dataLength, we make an evaluation if the characters exists then we place the length of the property results if not we place 0.
<InfiniteScroll
dataLength={characters ? characters.results.length : 0}
next={() => fetchNextPage()}
hasMore={!!hasNextPage}
loader={<Loading />}
/>
Now inside the component we must place the list to render, like this:
Now instead of iterating over data?.pages[0].results we are going to iterate over the memorized constant characters evaluating if it exists.
<InfiniteScroll
dataLength={characters ? characters.results.length : 0}
next={() => fetchNextPage()}
hasMore={!!hasNextPage}
loader={<Loading />}
>
<div className="grid-container">
{
characters && characters.results.map(character => (
<Card key={character.id} character={character} />
))
}
</div>
</InfiniteScroll>
And so everything would be complete:
import { useMemo } from "react";
import InfiniteScroll from "react-infinite-scroll-component"
import { useInfiniteQuery } from "@tanstack/react-query";
import { Loading } from "./components/Loading"
import { Card } from "./components/Card"
import { ResponseAPI } from "./interface"
const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())
const App = () => {
const { data, error, fetchNextPage, status, hasNextPage } = useInfiniteQuery(
['characters'],
({ pageParam = 1 }) => fetcher(pageParam),
{
getNextPageParam: (lastPage: ResponseAPI) => {
const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
const currentPage = previousPage + 1;
if (currentPage === lastPage.info.pages) return false;
return currentPage + 1;
}
}
)
const characters = useMemo(() => data?.pages.reduce((prev, page) => {
return {
info: page.info,
results: [...prev.results, ...page.results]
}
}), [data])
if (status === 'loading') return <Loading />
if (status === 'error') return <h4>Ups!, {`${error}` as string}</h4>
return (
<div>
<h1 className="title">React Infinite Scroll</h1>
<InfiniteScroll
dataLength={characters ? characters.results.length : 0}
next={() => fetchNextPage()}
hasMore={!!hasNextPage}
loader={<Loading />}
>
<div className="grid-container">
{
characters && characters.results.map(character => (
<Card key={character.id} character={character} />
))
}
</div>
</InfiniteScroll>
</div>
)
}
export default App
And this is what it would look like.
🎈 Refactoring.
Let's create a new folder src/hooks
and add the useCharacter.ts file.
And we move all the logic.
import { useMemo } from "react";
import { useInfiniteQuery } from "@tanstack/react-query";
import { ResponseAPI } from "../interface";
export const useCharacter = () => {
const { data, error, fetchNextPage, status, hasNextPage } = useInfiniteQuery(
['characters'],
({ pageParam = 1 }) => fetch(`https://rickandmortyapi.com/api/character/?page=${pageParam}`).then(res => res.json()),
{
getNextPageParam: (lastPage: ResponseAPI) => {
const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
const currentPage = previousPage + 1;
if (currentPage === lastPage.info.pages) return false;
return currentPage + 1;
}
}
)
const characters = useMemo(() => data?.pages.reduce((prev, page) => {
return {
info: page.info,
results: [...prev.results, ...page.results]
}
}), [data])
return {
error, fetchNextPage, status, hasNextPage,
characters
}
}
Now in src/App.tsx
it is easier to read.
import InfiniteScroll from "react-infinite-scroll-component"
import { Loading } from "./components/Loading"
import { Card } from "./components/Card"
import { useCharacter } from './hooks/useCharacter';
const App = () => {
const { characters, error, fetchNextPage, hasNextPage, status } = useCharacter()
if (status === 'loading') return <Loading />
if (status === 'error') return <h4>Ups!, {`${error}` as string}</h4>
return (
<div>
<h1 className="title">React Infinite Scroll</h1>
<InfiniteScroll
dataLength={characters ? characters.results.length : 0}
next={() => fetchNextPage()}
hasMore={!!hasNextPage}
loader={<Loading />}
>
<div className="grid-container">
{
characters && characters.results.map(character => (
<Card key={character.id} character={character} />
))
}
</div>
</InfiniteScroll>
</div>
)
}
export default App
🎈 Conclusion.
The whole process I just showed, is one of the ways you can implement infinite scroll in a fast way using third party packages. ♾️
I hope I helped you understand how to realize this design,thank you very much for making it this far! 🤗❤️
I invite you to comment if you find this article useful or interesting, or if you know any other different or better way of how to implement an infinite scroll. 🙌
🎈 Live demo.
https://infinite-scroll-app-fml.netlify.app
🎈 Source code.
Franklin361 / infinite-scroll
Creating an infinite scroll with react js ♾️
Creating an infinite scroll with React JS! ♾️
This time, we are going to implement the infinite scroll layout using React JS and other libraries!
Features ⚙️
- View cards.
- Load more cards while scrolling.
Technologies 🧪
-
▶️ React JS (version 18) -
▶️ Vite JS -
▶️ TypeScript -
▶️ React Query -
▶️ Rick and Morty API -
▶️ CSS vanilla (Los estilos los encuentras en el repositorio al final de este post)
Installation 🧰
- Clone the repository (you need to have Git installed).
git clone https://github.com/Franklin361/infinite-scroll
- Install dependencies of the project.
npm install
- Run the project.
npm run dev
Top comments (2)
Thank you for sharing this information, it will be very useful for me!
The problem with this infinite scroll implementation (which unfortunately is an issue with MOST implementations of the feature) is that it is lazy and has no regard at all for the amount of memory or resources it uses up. What is the point of having every single last item that has been loaded always in the page? When you are a long way down the list there could be literally many thousands of elements in the page that are essentially no longer going to be used/viewed - yet they are still there taking up resources.