We will see how to implement infinite scrolling using just React. For this we will be using the useCallback
hook and IntersectionObserver
dom api.
let’s start by simply creating the app
npx create-react-app infinite_scroll
cd infinite_scroll
yarn start
After you’ve cleaned up the boilerplate code, let’s start by creating our custom hook
touch src/useSearch.js
import{ useEffect, useState }from"react";import axios from"axios";exportdefaultfunctionuseSearch(query, page){constURL='http://openlibrary.org/search.json'useEffect(()=>{axios({
method:'GET',
url:URL,
params:{q: query, page}}).then(res=>{
console.log(res.data)})},[query, page]);}
we will be using the OpenLibrary api for our demo, and axios
to do the api calls for us.
The OpenLibrary api is paginated, so it’s perfect for our use case
In your browser, paste the url https://openlibrary.org/search.json?q=test23&page=1
to inspect the api end point
{"numFound":7,// number of results found"start":0,"numFoundExact":true,"docs":[{"key":"/works/OL8944247W","title":"MTTC French Sample Test 23","title_suggest":"MTTC French Sample Test 23","has_fulltext":true,"edition_count":1,
...
},{// book 2},{// book 3},
....
],"num_found":7,"q":"test23",// search keyword"offset":null}
So, let’s modify our custom hook
import{ useEffect, useState }from"react";import axios from"axios";exportdefaultfunctionuseSearch(query, page){constURL='http://openlibrary.org/search.json'const[books, setBooks]=useState([])const[isLoading, setIsLoading]=useState(false)const[loadMore, setLoadMore]=useState(false)useEffect(()=>{setBooks([])},[query])useEffect(()=>{setIsLoading(true);axios({
method:'GET',
url:URL,
params:{q: query, page}}).then(res=>{setBooks(prev=>{return[...prev,...res.data.docs.map(b=> b.title)]})setIsLoading(false)setLoadMore( res.data.docs.legth >0)})},[query, page]);return{isLoading, books, loadMore}}
When, we are getting back results from the api, we append it in our books
state
setBooks(prev=>{return[...prev,...res.data.docs.map(b=> b.title)]})
this will append the data with our previous state, so if we keep calling data for the next page, it’ll get appended to our previous result, rather than getting overwritten.
we use a different useState
to clear our books state when we query for different data
useEffect(()=>{setBooks([])},[query])
also, we set the isLoading
key to prevent our app to keep calling the api constantly.
The last part setLoadMore
is to let our app know we have reached the end of the paginated results, and stop doing api calls. Or, if we haven’t reached the end yet, and we can call for more
setLoadMore( res.data.docs.legth >0)
Finally we return the value of isLoading
, books
, loadMore
to our app.
Now, let’s import it to our App.js
import'./App.css';import{ useState }from'react';import useSearch from'./useSearch';functionApp(){const[query, setQuery]=useState('')const[page, setPage]=useState(1)const{isLoading, books, loadMore}=useSearch(query, page)// load our custom hookreturn(<div className="App"><input onChange={handleSearch} placeholder="search"></input><div className="results"></div></div>);}exportdefault App;
Time to modify our UI, first we add the handleSearch
function that will fire on onChange
event of our Input
, we just call to setQuery
to whatever data is in the Input
box
consthandleSearch=(e)=>{if(e.target.value ===''){returnnull;}setQuery(e.target.value)setPage(1)}
Next, we have our books
array in state, we’ll just loop through it and render the names
constrenderBooks=()=>{return books.map((book, index)=>{return(<p key={index}>{book}</p>)})}
and we call it inside results
div
<div className="results">{renderBooks()}</div>
Now, we will start our infinite scroll part. As we said earlier, we’ll be using the useCallback
hook, so let’s import it
import{ useState, useRef, useCallback }from'react';const observer =useRef()
it’ll be empty in the beginnig. Let’s think how will are we gonna implement it.
imagine, we have 5 books
<div><p>book</p><p>book</p><p>book</p><p>book</p><p>book</p></div>
We wan’t our api to be called for more when we reach the last book element. Something which will observe the Book#5
, when it get’s rendered in the screen, we will call for 5 more books, and the observer will then be set to Book#10
and removed from Book#5
We use the IntersectionObserver
dom api to do it for us.
const lastElementRef =useCallback((node)=>{if(isLoading){returnnull;// do nothing if already an api call going}if(observer.current){
observer.current.disconnect();// if we already have an observer.current (Book#5), then disconnect the observer from it}
observer.current =newIntersectionObserver(elements=>{if(elements[0].isIntersecting){// isIntersecting means the browser is intersecting with this current element, ie: showing in view
console.log("visible")}})if(node){
observer.current.observe(node)// set the observer to the new element (Book#10)}},[isLoading])
Now, we just need to add reference to this callback to the last element when we render, let’s modify our code
constrenderBooks=()=>{return books.map((book, index)=>{if(books.length === index+1){return(<p ref={lastElementRef} key={index}>{book}</p>)// add the callback when our index is the last one}return(<p key={index}>{book}</p>)// don't add callback otherwise})}
Now, our code is almost finished. Instead of doing console.log()
we just need to fire our api call.
observer.current =newIntersectionObserver(elements=>{if(elements[0].isIntersecting){setPage(prevPage=> prevPage +1)}})
That’s it.
Study material
souurce code
Nguồn: viblo.asia