This is my favorite project extension because this feature significantly improves the user experience in the app. Instead of clicking the "next" button, the user can scroll infinitely (as long as there's additional data in the database) to see more posts.
Table of Contents
- Demo
- Overview
- Enable scroll trigger through styling
- Post fetching
- Conclusion
Demo
You can check on the full source code and try them in Replit.
Repl URL: https://replit.com/@arnoldschan/PostPagination
Overview
User flow
As a user, they can explore posts by:
- Scroll to the bottom of the page, then
- Loading for fetching function to be finished, then
- New posts are appended below the last seen post
File tree:
This is how the project file tree looks like:
- The main
App.jsx
andApp.css
are in the root folder - The smaller components in
components/
folder - Components' css in
css/
folder - Anything related to firebase is inside
firebase/
folder
Enable trigger through styling
There are multiple ways to trigger an action when the user scrolls. In this example, we implement the scroll listener in the most outside component. We can simply use onScroll
:
//App.jsx
//..
<div className="app__post_view"
onScroll={checkBottom}>
<h1>Post Pagination Example</h1>
<div className="app__post_wrapper" >
// all of the posts here
</div>
// bottom part
</div>
We call checkBottom
function if the user scrolls.
We implement the event listener inside a div
component, not the whole window
component. A little adjustment to the component's styling is needed. Here's how we style the component:
/* App.css */
/* ... */
.app__post_view {
/* other important styles */
overflow-y: scroll;
height: 100vh;
}
/* ... */
We need to limit the height of the component to 100% of the user's viewport height. Any vertical overflow is scrollable. This way, we implement the scrolling mechanism in the component, not to the window
. Here's the visualization:
As what I mentioned before, there are so many ways to trigger action when the user scrolls. In the left figure, we simply add the listener in the browser window. But in this example, we add the scroll listener in the component (right side figure).
Post fetching
State hooks
// App.jsx
// ..
const [posts, setPosts] = useState([])
const [docs, setDocs] = useState([])
const [fetching, setFetching] = useState(false);
const [morePost, setMorePost] = useState(true)
There are 4 state hooks used in this example:
-
posts
state stores all of the fetched posts data -
docs
stores all of the fetched posts Firebase documents (we actually can replaceposts
with this one, this hooks was later added in the project) -
fetching
tells whether our app is still waiting for downloading the additional posts or not -
morePost
istrue
if there is any post that the user hasn't seen. Otherwise, all of the posts in the database have been browsed by the user.
Bottom check function checkBottom
Now, let's check into checkBottom
function triggered by user's scroll.
// App.jsx
// ...
const checkBottom = (e) => {
const bottom = (
(e.target.scrollHeight
- e.target.scrollTop
=== e.target.clientHeight) &
(fetching === false) &
(morePost === true));
if (bottom) {
fetchData()
}
}
//..
This function calls fetchData
function if the user hit the bottom end of the component. Moreover, it only calls if the app not in the middle of fetching
process and there is additional posts in the database through morePost
state.
Fetch posts fetchData
// App.jsx
import { db } from "./firebase/firebase";
// ..
const fetchData = () => {
if (fetching === true) return;
setFetching(true);
let query = db
.collection('posts')
.orderBy('timestamp','desc')
//.. this block enables pagination
if (posts.length !== 0) {
const lastVisible = docs[docs.length-1];
query = query
.startAfter(lastVisible);
}
query.limit(5)
.get().then(snapshot=>{
if (snapshot.docs.length === 0) setMorePost(false);
setDocs([...docs, ...snapshot.docs])
setPosts([...posts, ...snapshot.docs.map(doc=> (
{id: doc.id,
post: doc.data()}
))])
}).then(
setFetching(false)
);
}
We extend the existing fetch function from the first series with pagination capability. First, we should check fetching
state is in false
to avoid multiple fetches.
In the first fetch, we'll skip the middle if
statement. We simply fetch the posts data from db
(Firestore object) ordered by timestamp and limit by 5 posts in each fetch. After we got the data, we update docs
and posts
states then sequentially switch the fetching
state to false
.
In the second fetch and after, this function considers the if
statement in the middle. Here, we update the query
object with startAfter
attribute, telling query
what was the latest fetched data in lastVisible
. The rest of the function is exactly the same as the first fetch.
Conclusion
We can easily mimic infinite scroll like we always enjoy from many social media platforms. In this example, we apply it through scroll listener and simple styling in the component.
We also need to paginate in every API call to get additional posts. Do you have any better alternative in implementing infinite scroll? Drop your thoughts below!
Top comments (2)
Great work.....simple and nice
Thank you! Really appreciate it.