Snapgram Infinite Scroll
It's time to implement the most interesting feature of all time that has captivated us for many years, i.e., mindless, endless scrolling on Instagram.
Task 🎯
Your mission is to think and try the Infinite scroll in the application for the Home, Users & Profile pages.
Example 🧩
Although I don't have to say this, but visit the Instagram website/app and see how new posts/images get added every time you scroll down.
Resources 📖
-
Query Key
-
Query Function
-
Infinite Query
-
Appwrite Pagination
-
Appwrite Queries
-
Appwrite List Documents
-
React Intersection Observer
Must Read ❤️🔥
Check an example provided by React Query on Infinite Scroll.
Hint 💡
Not sure how to get started? We've got you covered.
As I always say, start by thinking on the Backend!
What do we need? - The Output A feature that will automatically fetch the next posts/items/documents when we reach the end of the list we currently have.
What are the things that we'll need? We somehow need a function that will perform this action and give us the results we want, right? So what should this function know in order to perform the action?
- Database ID?
- Collection ID? (Which collection we're querying)
- A few fixed elements at a time (limit)
- ...? (Think!)
But how should it work overall? Let's take the Instagram example again. How does it work?
- The user visits the Explore page (as an example)
- The user sees some posts (initial)
- The user scrolls down and sees more posts.
Does that mean Instagram is showing many posts in one request? No, it's basically sending only certain posts initially, and when the user reaches the end of the list, it makes another request to fetch and display the next batch of posts.
So basically, there is a "Load More" button, but it's invisible and gets called automatically by the code. The user doesn't have to click it to load more posts.
Yes, that's it. That's how pagination works. It's the same thing. But instead of letting the user click on "Load More" to get more posts, we do that on their behalf through the code.
So how can we implement it? Luckily, React Query provides us with a hook called useInfiniteQuery to implement features like infinite scroll. So let's start the process by implementing it:
- Create a new query, say,
useGetUsers
, using theuseInfiniteQuery
using React Query.export const useGetUsers = () => {return useInfiniteQuery();// ...}; - Provide a nice
queryKey
(like a name tag for the data you want to fetch, making it easy to find and work with that data).export const useGetUsers = () => {return useInfiniteQuery({queryKey: ["getInfiniteUsers"],// ...});}; - Create a new function to fetch the data and assign it to the
queryFn
. For example,getUsers
. This function will execute when we call React Query.export const useGetUsers = () => {return useInfiniteQuery({queryKey: ["getInfiniteUsers"],queryFn: getInfiniteUsers,// ...});}; - Set up the getNextPageParam function that React Query provides us when using useInfiniteQuery for calculating the next page.
export const useGetUsers = () => {return useInfiniteQuery({queryKey: ["getInfiniteUsers"],queryFn: getInfiniteUsers,getNextPageParam: () => {},});};
All done?
Now let's implement the queryFn
that we had created, as that's what's going to execute whenever we call this new hook we're creating using React Query.
-
Use the try-catch block and throw the error.
export async function getInfiniteUsers() {try {// ...} catch (error) {console.log(error);}} -
Inside the try, implement the
listDocuments
function of Appwrite on the collection whose documents we want to list or fetch.export async function getInfiniteUsers() {try {const users = await databases.listDocuments(appwriteConfig.databaseId,appwriteConfig.postCollectionId,);// ...} catch (error) {console.log(error);}} -
Limit the number of documents to receive using the
Query.limit
query -
Return the posts.
export async function getInfiniteUsers() {try {const users = await databases.listDocuments(appwriteConfig.databaseId,appwriteConfig.postCollectionId,[Query.limit(20)],);if (!users) throw Error;return users;} catch (error) {console.log(error);}}
Now, let's get back to our newly created useGetUsers
query and implement the getNextPageParam
.
React Query will call this function when we want to fetch the next batch of posts.
So, how do we get the next batch of documents using Appwrite? If you see the above link on Appwrite Pagination, you'll know that Appwrite provides two different ways for doing pagination, i.e., offset and cursor. Which should we go with then?
If you scroll down a bit, they recommend using the cursor pagination for the pages with infinite scrolling. And that's what we're doing.
So how do we implement the cursor pagination? If you scroll up a bit, you'll see the cursor pagination example.
It says,
After reading a page of documents, pass the last document's ID into the Query.cursorAfter(lastId)
query method to get the next page of documents.
In simple words, we'll have to pass the ID of the last document we have fetched in order to get the next batch of documents.
We'll do the same in our getNextPageParam
!
React Query will pass the last page details to this special function, from which we can get the last ID of the document and return that last ID. This last ID will then be passed to whatever function we'll assign to queryFn
as a pageParam
parameter.
Let's take it step by step,
-
Go to
getNextPageParam
function of the query we created. -
Get the last page information via
lastPage
param.export const useGetUsers = () => {return useInfiniteQuery({queryKey: [QUERY_KEYS.GET_INFINITE_POSTS],queryFn: getInfiniteUsers,getNextPageParam: (lastPage) => {// ...},});}; -
Check if there is the last page.
- If not, return.
- If yes, calculate the last ID from the
lastPage
parameter (P.S., it'll be the same as what Appwrite has shown in its example. If you're not sure, simply console log whatlastPage
holds).
export const useGetUsers = () => {return useInfiniteQuery({queryKey: [QUERY_KEYS.GET_INFINITE_POSTS],queryFn: getInfiniteUsers,getNextPageParam: (lastPage) => {// If there's no data, there are no more pages.if(...) {}// Use the $id of the last document as the cursor.const lastId = ...},});}; -
Return the calculated ID.
Now let's get back to our queryFn
. React Query sends the data that it has fetched from the list, page, and pageParam
to this queryFn
. You can check what it sends to queryFn
here.
It sends a data object containing different fields. Out of which one is pageParam
containing the page params used to fetch the pages (In our case, holding the last ID of the document we fetched).
Now in our queryFn
.
- Create a queries array.
- Check if there is
pageParam
,- If yes, push the
Query.cursorAfter
to it. - If not, leave.
- If yes, push the
- Then use this query inside the
listDocuments
to query the collection inside the database according to the defined queries.
export async function getInfiniteUsers({ pageParam }) {
const queries = [Query.limit(20)];
if (pageParam) {
queries.push(...);
}
try {
const users = await databases.listDocuments(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
queries
);
if (!users) throw Error;
return users;
} catch (error) {
console.log(error);
}
}
That's basically the setup. Easy peasy.
But how do we use it on UI?
- Call our newly created query, i.e.,
useGetPosts
inside the page. - Along with data, we'll have access to these,
fetchNextPage
: calls ourqueryFn
to get the next page depending on thegetNextPageParam
function values (i.e., newly createdpageParam
)hasNextPage
: returns a booleantrue
ifgetNextPageParam
returns a value other thanundefined
const { data: users, fetchNextPage, hasNextPage } = useGetUsers();
Okay, we're very close. Hope you're still here.
You might say, "Okay, I did everything, but how do I call the fetchNextPage automatically? Are we gonna call it manually?" Nah, nope, nada, no!
This is where a special package comes into play, i.e., React Intersection Observer. Using the hooks of this package, we can determine if an element is in the user's view or not, i.e., if the user is seeing that element or not.
But hey hey, how's that helping with our problem? It's the main thing! Imagine, creating a small "Load More" button at the end of the list on the UI and using the above package to see if this button element is in the user's view; If yes - call the function, if not, leave it.
Make sense?
That’s what we’ll do,
- Go to the page where we want to implement the query.
- Install the React Intersection Observer.
- Use the
useInView
hook from React Intersection Observer. - It'll give you two things, i.e.
ref
: to refer to the element we want to track.inView
: tells if the element is in view or not.
const { ref, inView } = useInView(); - Add this ref as a ref to our button/loader element.
<div ref={ref}><Loader /></div>
- Create a
useEffect
to keep track of knowing whether the element is in view.- If yes, call the
fetchNextPage
. - If not, leave.
useEffect(() => {if (inView) {fetchNextPage();}}, [inView]); - If yes, call the
- As a final condition, show this button only when there is
hasNextPage
, otherwise hide it.
Yeah, that's how we do it. Now test the functionality.
If it's not working as expected, don't feel frustrated. It's part and parcel of the developer's life. Start again and debug everything line by line. See if you're getting documents, and check if the last ID is being calculated properly and whether it's being passed to the queryFn
properly or not.
Keep referring to the docs while developing the logic, as they clearly provide examples of how to do it the right way.
Keep trying 🚀
These challenges or features are more logic-oriented, and there may be other solutions. So take your time, think carefully, develop your logical abilities, and give it a try.
We believe in your capabilities ❤️