In this tutorial, we will learn how to fetch and render data in chunks with infinite scrolling and how to integrate and use the meilisearch plugin to search for books.
Author: @tammibriggs
Fetching large sets of data at once can cause some negative effects like making components render slowly, which creates a bad user experience for site visitors. To handle this, two patterns are commonly used among which is infinite scrolling, which we will be covering in this tutorial.
Goal
In this tutorial, we will be building a book app using Strapi. The app will focus on how to fetch and render data in chunks with infinite scrolling and how to integrate and use the meilisearch plugin to search for books.
Prerequisites
To follow along with this tutorial, you should be familiar with React and you should have Node installed in your system.
An Introduction to Strapi
Strapi is an open-source, headless Content Management System(CMS) developed using the Nodejs Javascript framework that allows designing API fast and can be accessed from any client(React, Vue, etc), giving developers the freedom of using their native tools.
Strapi includes a user-friendly Admin page that provides feature for easy management and monitoring of content. The Admin page, as well as created API, can be customized to match our use cases based on its plugin system which is one of the endearing features of Strapi.
Setting up a Strapi Project
Setting up a Strapi project is pretty straightforward. Just like create-react-app
, Strapi has [create-strapi-app](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/installation/cli.html#creating-a-strapi-project)
.
Run the following commands:
$ mkdir book-app
$ cd book-app
$ npx create-strapi-app@latest book-app-backend --quickstart
The command above sets up Strapi for our app with all required dependencies and creates a new folder book-app-backend
.
Once the installation is complete, the server will start up which we can view in our browser via the specified link. In our browser, we the following page:
Here, fill in the required details and create a user account to access the dashboard.
Creating the Collection Types
In the collection type, we will define the content we wish to store on Strapi. To do this, first, click on the Create your first Collection Type button.
On this page, click on Create new collection type and we will see a prompt asking to input the Display name and other information:
Enter Books as the display name and click on Continue. On the next prompt, we will see different field types we can create for our collection.
Here, we will create fields for the books. Each book will have authors, description, image, previewLink, publishDate, publisher, subtitle and title. These are the fields our Books collection type will include. All the fields will be a text fields except for authors which will be of type JSON.
Above are all the fields for the book data. After creation, click on the Save button at the top right of the page.
Building a Book App
The book app will have a homepage that will display all the books available in our Strapi collection which we will fetch in chunks using infinite scrolling. We will have a detail page to display information about a particular book, and a search page that displays the search results received from Meilisearch.
I have already created a stater repo for the book app with the template we will use and the book data to be added Strapi which I fetched from the Google book API.
Next, we need to clone the starter GitHub repo. In the terminal, cd into the book-app directory we created earlier and type in the following lines of code:
$ git clone -b starter https://github.com/Tammibriggs/strapi-book-app.git
$ cd strapi-book-app
$ npm install
Now, when we start our app with the $ npm start
command, we will see this page:
If we click on a book, we will be taken to the detail page which looks like this:
Right now, we are getting our book data from the data.js file in the src directory of the cloned app. We will be moving the book data over to Strapi and fetching it from there shortly using meilisearch and implementing infinite scrolling using the Intersection Observer API.
Adding Data to Strapi
In the data.js file in the src directory, we have over fifty-four (54) book data; let’s move them over to Strapi. To do this, we need to first allow access to the Strapi collection. Navigate to the dashboard in Settings at the sidebar. Select Roles under Users and Permissions. Click on Public, select Book, and check all checkboxes.
Then, click on the Save button at the top-right to save these changes.
Next, in the src/pages/Home.js add the following import:
import axios from 'axios'
We're able to import axios here because it was included in the starter app. Next, add the following lines of code after the books
state in the Home
component:
// src/pages/Home.js
const URL = "http://localhost:1337/api/books"
useEffect(() => {
sendData()
}, [])
const sendData = async () => {
let fetchedData;
const fetchCol = await axios.get(URL)
fetchedData = fetchCol.data.data
if (!fetchedData.length) {
console.log('done')
try {
books.forEach((book) => {
axios.post(URL,{
data: {
authors: book.authors,
description: book.description,
image: book.image,
previewLink: book.previewLink,
publishDate: book.publishDate,
publisher: book.publisher,
subtitle: book.subtitle,
title: book.title,
}})
})
console.log('done')
} catch (error) {
console.log(error);
}
} else {
console.log("data already uploadedd")
}
}
The above code checks if there is any data in our Strapi collection, and if there is not, it populates our the collection with all data in the data.js file.
Now when we head over to our Strapi dashboard and click on Content Manager in the sidebar, we see fifty-four (54) entries in our Books collection.
Next, we will integrate and use meilisearch to get our books data from our Strapi collection and display it, and will also implement the search functionality. To search for data, meilisearch uses a query passed to it. when the query is empty it will return all the books in our collection which we will display on the home page and when the query is not empty it returns the corresponding result.
Integrating Meilisearch
To use Meilisearch locally, we will download and run an instance of it. This can be downloaded here. Opening the downloaded application shows a terminal with the Meilisearch instance hosted on local host:
If we navigate in the browser to the specified URL, we will see the Meilisearch interface.
Next, we a different terminal cd into the book-app-backend directory and install the Strapi-meilisearch
plugin with the following command:
$ npm install strapi-plugin-meilisearch
After this, we re-run npm run develop
to rebuild our Strapi application with the new meilisearch
plugin. When we open the localhost URL in our browser and log in, we will be directed to the Strapi dashboard:
Next, let's click on the meilisearch
option on the sidebar, and in the Settings tab enter the URL for the meilisearch instance.
Click on save. Now, add the book collection to meilisearch in the Collections section by click on the check box:
With this, when we refresh the meilisearch instance, we will see the entries in our Strapi collection.
Fetching Books Data from Meilisearch
To fetch our book data in our frontend, we can either use the search routes provided for us (for example, this will fetch 30 book data: http://127.0.0.1:7700/indexes/book/search?limit=30) or we can use meilisearch package. In this tutorial, we will be using the package so we will need to first install it.
- In the terminal, cd into strapi-book-app and type in the following command:
$ npm install meilisearch
- Next, add the following import to the Home.js file in src/pages:
import MeiliSearch from "meilisearch";
- Next, modify the book state by replacing Allbooks with an empty array. It should look like this:
const [books, setBooks] = useState([])
- Now, add the following lines of code after the
books
state:
// src/pages/Home.js
const fetchData = async () => {
const client = new MeiliSearch({
host: 'http://127.0.0.1:7700',
})
const index = await client.getIndex('book')
const booksData = await index.search('*')
setBooks(booksData.hits)
}
The above function, when called, returns our data from the Strapi book collection received through the meilisearch
instance. Notice that in the search
method we are passing ***** as the query. This will fetch all our data with a limit of twenty (20) which is the default. This can be customized.
To search for a particular data we just need to pass it to the search
method. We will use this to implement our search functionality.
We want the above function to be called when our app renders so we will call it in a useEffect
hook. In the Home
component, Modify the useEffect
hook with the sendData()
function to now look like this:
// src/pages/Home.js
useEffect(() => {
fetchData()
sendData()
}, [])
With this, the data from our Strapi book collection should now be displayed in our app. Next, let’s make sure that when a book card is clicked we are getting the details of that particular book.
To do this,
- Head over to the src/pages/BookDetail.js and first add the following import:
import MeiliSearch from 'meilisearch'
- Next, modify the
useEffect
hook to look like this:
// src/pages/BookDetail.js
useEffect(() => {
const fetchData = async () => {
const client = new MeiliSearch({
host: 'http://127.0.0.1:7700',
})
const index = await client.getIndex('book')
const bookData = await index.getDocument(params.id)
setBook(bookData)
}
fetchData()
}, [])
With this, when we click on a book we should see the book details.
Implementing the Search Functionality
For the search functionality, when we type in a query in the search bar, it will take us to the search page attaching the query to the URL. We will get that query and pass it to the search method of meilisearch which will then return the corresponding results:
To do this,
- Go over to src/pages/Search.js and first add the following imports:
// src/pages/Search.js
import MeiliSearch from 'meilisearch'
import {useEffect, useState} from 'react'
- Next, add the following lines of code after the
params
variable in theSearch
component:
// src/pages/Search.js
const [books, setBooks] = useState([])
useEffect(() => {
const fetchData = async () => {
const client = new MeiliSearch({
host: 'http://127.0.0.1:7700',
})
const index = await client.getIndex('book')
const booksData = await index.search(params.query)
setBooks(booksData.hits)
}
fetchData()
}, [params.query])
The above code will return all the matching results based on the search query and set it into the books
state. Now let's render the fetched results.
- Modify the div in the return statement to now look like this:
// src/pages/Search.js
<div className='searchPage wrapper'>
<div className='books'>
{books?.map((book) => (
<Book
key={book.id}
title={book.title}
image={book.image}
authors={book.authors}
publisher={book.publisher}
publishDate={book.publishedDate}
id={book.id}
/>
))}
</div>
</div>
With this, when we search for books in the search bar we will see the results in the search page.
Implementing Infinite Scrolling with the Intersection Observer API
For the infinite scrolling functionality, we will limit the returned book from meilisearch to fifteen then when we scroll to the bottom of our page we will fetch and append another fifteen data.
To do this, we will use the Intersection Observer API to know when we have got to the bottom of our page, then to return books in chunks of fifteen from meilisearch we will use the limit and the offset parameter which can be specified in the object passed as the second parameter of the search
method.
Fetching New Data Using Intersection Observer
The Intersection Observer API monitors when an observed element is visible or when it reaches a predefined position and then it fires the callback function supplied to it. To use this API we will first create an element at the bottom of our fetched data which will be the observed element. Then, when this element is visible we will call the callback function which will be responsible for getting and pending new book data.
- In the Home.js file add the following imports:
import {useRef, useCallback} from 'react'
- After this, add the following lines of code after the closing tag (
</div>
) of thediv
with the className of books.
// src/pages/Home.js
<div className='loader' ref={observerElem}>
{books.length !== 0 &&
<span>{hasNextPage ? 'Loading...' : 'no books left'}</span>
}
</div>
Above, we created the div
element we want to observe using Intersection Observers. We have added the ref
attribute so we can be able to access it directly. The above div
will display **Loading…* or no books left ***depending on hasNextPage
which will be a boolean state that will be true or false depending on whether there is still data to be fetched.
- Next, add the following line of codes after
URL
variable:
// src/pages/Home.js
const observerElem = useRef(null)
const [hasNextPage, setHasNextPage] = useState(true)
- Next, add the following lines of code after the
hasNextPage
state:
// src/pages/Home.js
const handleObserver = useCallback((entries) => {
const [target] = entries
if(target.isIntersecting && hasNextPage) {
console.log('intersected')
}
}, [hasNextPage])
useEffect(() => {
const element = observerElem.current
const option = { threshold: 0 }
const observer = new IntersectionObserver(handleObserver, option);
observer.observe(element)
return () => observer.unobserve(element)
}, [hasNextPage, handleObserver])
The above code will detect when the observed element has entered the viewport and then call the handleObserver
callback function. Now when we scroll to the bottom of our page in the browse and we check in the console **intersected* will be logged.*
Next, let’s create the function that will fetch and append book data whenever we scroll to the bottom of our page. For this we will modify the fetchData
function to get fifteen new books data any time is called using the limit and offset parameter, then we will append newly fetched books to the books
state.
To do this,
- First, add the following code after the
hasNextPage
state:
// src/pages/Home.js
const [offset, setOffset] = useState(0)
const [lastPage, setLastPage] = useState({})
- Next, modify the fetchData function to now look like this:
// src/pages/Home.js
const fetchData = async () => {
const client = new MeiliSearch({
host: 'http://127.0.0.1:7700',
})
const index = await client.getIndex('book')
const booksData = await index.search('*', {
limit: 15,
offset: offset
})
setBooks([...books, ...booksData.hits])
setLastPage(booksData)
}
Next, we need to call the fetchData()
in handleObserver
so that when we scroll to the bottom of the page it will be called.
- Modify the
handleObserver
function to now look like this:
// src/pages/Home.js
const handleObserver = useCallback((entries) => {
const [target] = entries
if(target.isIntersecting && hasNextPage) {
fetchData()
}
}, [fetchData, hasNextPage])
- Finally, add the following line of code after the
fetchData
function:
// src/pages/Home.js
useEffect(() => {
setOffset(books.length)
if(books.length < lastPage.nbHits){
setHasNextPage(true)
}else{
setHasNextPage(false)
}
}, [books])
With this, we are done implementing our infinite scrolling functionality. When we scroll to the bottom of the page, new books will be displayed.
Conclusion
In this tutorial, we learnt how to implement infinite scrolling and search functionality in Strapi using meilisearch by building a book app.