How to Make Infinite Scroll on Hooks in React App
Introduction
Infinite Scrolling – is a popular method of loading data as needed (on-demand quests). In this paradigm, in the initial render, the application requests only a part of the content (only the one that it can see) and dynamically loads the next parts as the user scrolls the page, providing a seamless user experience.
This article describes the simplest way of implementation – using hooks.
React Hooks Library
reactuse – is the largest collection of hooks for React. To implement the idea, you will need 2 hooks from it:
useQuery – to create queries to the server
useIntersectionObserver – to track whether a DOM element is in the user's field of view
For installation:
$ npm i @siberiacancode/reactuse --save
# or
$ yarn add @siberiacancode/reactuse
Fake API
For demonstration I will use mock api – https://pokeapi.co/
Request https://pokeapi.co/api/v2/pokemon/?limit=5&offset=0
will give out the first 5 Pokemon.
Infinite Scroll Implementation
create a constant PORTION_OF_ITEMS, which will mean the number of Pokemon that will be returned in one request. (PORTION_OF_ITEMS = limit in the request)
const PORTION_OF_ITEMS = 4;
function App() {}
We create a state offset, which will store the offset of the search for Pokemon (we will shift by an amount equal to PORTION_OF_ITEMS)
const [offset, setOffset] = useState<number>(0);
Import hooks from reactuse
import { useIntersectionObserver, useQuery } from "@siberiacancode/reactuse";
We create a request using useQuery. In the callback function we specify a regular fetch for our API request. (Important: in the options object of the useQuery hook the following keys are specified: [offset]this is necessary so that the request is sent again if we update the offset state). We also create a pokemons state.
In case of success (onSuccess) we throw both the previous Pokemons and the ones just requested into the pokemons array.
-> We get a set of states: data, isLoading, isError, isSuccess, error.
const [pokemons, setPokemons] = useState<Pokemon[]>([]);
const { isLoading, isError, isSuccess, error } = useQuery(
() =>
fetch(
`https://pokeapi.co/api/v2/pokemon/?limit=${PORTION_OF_ITEMS}&offset=${offset}`
)
.then((res) => res.json())
.then((res) => res.results as Promise<Pokemon[]>),
{
keys: [offset],
onSuccess: (fetchedPokemons) => {
setPokemons((prevPokemons) => [...prevPokemons, ...fetchedPokemons]);
},
}
);
When isError display an error message. If isLoading display “Pending…”
If everything is OK (isSuccess), then we use the map() method on the pokemons variable array to display all the Pokemons.
After the list of Pokemons, we add a div “Loading new…”, we will refer to it later. If it gets into the field of view, then the data will be loaded.
if (isError)
return (
<div>
{error?.name}: {error?.message}
</div>
);
if (isLoading) return <div>Pending...</div>;
if (isSuccess)
return (
<div>
{pokemons.map((pokemon, index) => {
return (
<div key={index} className=" w-32 h-32">
<h1>{pokemon.name}</h1>
<img
alt={pokemon.name}
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${
index + 1
}.png`}
/>
</div>
);
})}
<div>Loading new...</div>
</div>
);
interface Pokemon {
name: string;
}
Now let's add useIntersectionObserver
Here we get a ref, which we will throw on a div element with the text “Loading…” so that when it appears in the field of view, the application immediately processes the next request.
in the method onChange we check if there is an intersection. If yes, we move the offset state by a value equal to PORTION_OF_ITEMS. (since we previously passed offset to keys, the useQuery hook parameter, a new query will be sent as soon as the offset state is updated)
const { ref } = useIntersectionObserver<HTMLDivElement>({
threshold: 1,
onChange: (entry) => {
if (entry.isIntersecting) setOffset((prev) => prev + PORTION_OF_ITEMS);
},
});
<div ref={ref}>Loading...</div>
Now everything is ready.
![](https://habrastorage.org/getpro/habr/upload_files/7f7/2bd/78f/7f72bd78faea9c40e97ce17fe7adf573.gif)
All code
import { useIntersectionObserver, useQuery } from "@siberiacancode/reactuse";
import { useState } from "react";
const PORTION_OF_ITEMS = 4;
interface Pokemon {
name: string;
}
function App() {
const [offset, setOffset] = useState<number>(0);
const [pokemons, setPokemons] = useState<Pokemon[]>([]);
const { isLoading, isError, isSuccess, error } = useQuery(
() =>
fetch(
`https://pokeapi.co/api/v2/pokemon/?limit=${PORTION_OF_ITEMS}&offset=${offset}`
)
.then((res) => res.json())
.then((res) => res.results as Promise<Pokemon[]>),
{
keys: [offset],
onSuccess: (fetchedPokemons) => {
setPokemons((prevPokemons) => [...prevPokemons, ...fetchedPokemons]);
},
}
);
const { ref } = useIntersectionObserver<HTMLDivElement>({
threshold: 1,
onChange: (entry) => {
if (entry.isIntersecting) setOffset((prev) => prev + PORTION_OF_ITEMS);
},
});
if (isError)
return (
<div>
{error?.name}: {error?.message}
</div>
);
if (isLoading && !pokemons.length) return <div>Pending...</div>;
if (isSuccess)
return (
<div>
{pokemons.map((pokemon, index) => {
return (
<div key={index} className=" w-32 h-32">
<h1>{pokemon.name}</h1>
<img
width={475}
alt={pokemon.name}
height={475}
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${
index + 1
}.png`}
/>
</div>
);
})}
<div ref={ref}>Loading...</div>
</div>
);
}
export default App;