By participating in this hackathon, I've technically learned how to build API in a totally different way than what I've used to which is REST API. I'm discovering Apollo methods such as fetchMore
which I think it's super cool!
So, one of the features I had in mind for the Goodeed app is infinite scrolling. Eventhough I don't have a lot of dumb data, I'd still love for this to be a new-thing-I've-learned.
In the midst of investigating on how to implement this, I found Apollo's fetchMore which suits my requirement perfectly, but not quite...
To jump start this project as quickly and as "cheap" as I can, I've decided to use MongoDB Atlas as the database so I don't have to worry much about DB and concentrate on the rest. One of the requirements for the app is to always sort the posts by the location of the user first (closest to farthest), then by recent date.
Since I've chosen to use the free tier cluster, there are some MongoDB methods that are not permitted such as aggregate
or mapReduce
which is what I would use IDEALLY. But oh well, I guess I'd have to use Javascript methods which is quicker and easier but not as performant and it looks like a dirty workaround ð
.
Okay so let's get started. First, I have a gql query of GET_POSTS
and I'm using Apollo's useQuery
to get posts when the page is mounted.
const GET_POSTS = gql`
query GetPosts($offset: Int) {
posts(offset: $offset) {
content {
...
}
hasMore
}
}
`;
const { loading, error, fetchMore } = useQuery(GET_POSTS, {
onCompleted: data => {
const { content, hasMore } = data.posts;
setPosts(content);
setHasMorePosts(hasMore);
},
});
Notice the fetchMore
in the useQuery destructured properties? This is a function/method given by Apollo to basically re-use the GraphQL query but you're free to pass the variables.
What I want is that when the user scrolls down and reaches the bottom of the page, I want to fetch more posts. So here's where the offset comes in. I have an onScroll
event that I've attached to the div
containing the posts like so
<div onScroll={onScroll}>...</div>
and here's the code for the implementation
const onScroll = e => {
// if div is at the bottom, fetch more posts
if (e.target.scrollTop + e.target.clientHeight === e.target.scrollHeight) {
// if there are no more posts to fetch, don't do anything
if (!hasMorePosts) return;
setTimeout(() => {
setIsFetchMoreLoading(true);
fetchMorePosts();
}, 300);
return;
}
};
const fetchMorePosts = async () => {
setOffset(prev => prev + 1);
const fetchedMore = await fetchMore({
variables: { offset: offset + 1 },
});
const { content, hasMore } = fetchedMore.data.posts;
setPosts(previousPosts => [...previousPosts, ...content]);
setHasMorePosts(hasMore);
setIsFetchMoreLoading(false);
};
Alright, it seems pretty straightforward. The onScroll
event will listen for scroll events and if the user is at the bottom of the page, I want to fetch more posts. However, if there's no more posts to fetch, I just want to return and do nothing.
In the fetchMorePosts
function, I'm calling Apollo's fetchMore
with new offset variable. I'll show what's happening in the resolver now.
const resolvers = {
Query: {
posts: async (_parent, { offset }, { db, loggedUser }, _info) => {
if (!loggedUser) throw new AuthenticationError('you must be logged in');
const { location } = await db.collection('users').findOne({ username: loggedUser.username });
let posts;
const allPosts = await db
.collection('posts')
.find()
.toArray();
const sortedByDate = allPosts.reverse();
// mongodb mapReduce not supported for free tier cluster :(
// Workaround -> handle sort using JS methods
if (location) {
const compareDistance = (postLat, postLng) => calcDistance(postLat, postLng, location.lat, location.lng);
// sort by distance (closest -> farthest)
posts = sortedByDate.sort(
(a, b) => compareDistance(a.location.lat, a.location.lng) - compareDistance(b.location.lat, b.location.lng)
);
} else {
posts = sortedByDate;
}
// sort by offset
if (offset) {
// if offset is not null (not the initial fetch)
const newOffsetValue = offset * FETCH_LIMIT;
const payload = posts.slice(newOffsetValue, FETCH_LIMIT + newOffsetValue);
return { content: payload, hasMore: !!payload.length };
} else {
const payload = posts.slice(0, FETCH_LIMIT);
return { content: payload, hasMore: !!payload.length };
}
}
}
}
The offset is from where do I want to start and limit is how many do I want. So, on the initial fetch where offset is undefined (which falls in the if(offset)
else block), I want to give them the posts from 0 - LIMIT (let's say 5). And the next query from fetchMore
will give us the offset of 1
, and now we will slice the array by (5,10)
.
I'm also returning hasMore
in the response as I wanted to handle the fetch more loading state on the frontend side.
I've learned a lot and will be doing so even more. It's also been a while since I've coded in React so... fair game. There's definitely a more efficient way of sorting the DB on the server and handling UI on the client (I chose to use react state instead of Apollo's inMemoryCache). Perhaps with more time and resources, these things can be improved :)
Until then, happy hacking! ð
Top comments (0)