At this point, we have working pagination. But we need to add some improvement in there. We need to add functionality to show and hide the load more button when there are no more stories to load.
To do that let’s change the backend code first.
First I am adding a @ObjectType
to get the Posts and the status of the remaining post as boolean.
@ObjectType()
class PaginatedPosts {
@Field(() => [Post]) // typeorm type
posts: Post[]; // typescript type
@Field()
hasMore: boolean;
}
The logic:
The count of posts that we request from the database will be always by adding one to the max post count that we define. If we get that number of the post means there are more post to come from next request. Here is the relevant code for it. Also, I added some comment in the below code to explain it.
@Query(() => PaginatedPosts) // change the return type
async posts(
@Arg("limit", () => Int) limit: number,
@Arg("cursor", () => String, { nullable: true }) cursor: string | null
): Promise<PaginatedPosts> { // change the return type
// return await Post.find();
// using query builder
const realLimit = Math.min(50, limit);
const realLimitPlusOne = realLimit + 1; // get max + 1 posts
const qb = getConnection()
.getRepository(Post)
.createQueryBuilder("p")
.orderBy('"createdAt"', "DESC")
.take(realLimitPlusOne);
if (cursor) {
qb.where('"createdAt" < :cursor', {
cursor: new Date(parseInt(cursor)),
});
}
const posts = await qb.getMany();
return {
posts: posts.slice(0, realLimit), // slice the post array to return actual limit
hasMore: posts.length === realLimitPlusOne, // set the boolean to indicate for there are more post or not
};
}
Once we added the above code, we completed the back-end code for the application. Let’s move to the front-end app and start to add the code.
First’ change the graphql
query to match with the new return type from the back end.
query Posts($limit: Int!, $cursor: String) {
posts(cursor: $cursor, limit: $limit) {
hasMore // new return type value
posts{ // new return type posts
id
createdAt
updatedAt
title
textSnippet
}
}
}
Now run generate command to generate the TypeScript types for this new mapping type.
yarn gen
Now we need to change cursorPagination
resolver. Also, I added some comments in there for more explanation
const cursorPagination = (): Resolver => {
// ... rest of the code
// find the posts array from the cache
const isItInTheCache = cache.resolve(
cache.resolve(entityKey, fieldKey) as string, find the keys
"posts"
);
// ...rest of the code
let hasMore = true; // add new variable to keep the hasMore
fieldInfos.forEach((fi) => {
const key = cache.resolve(entityKey, fi.fieldKey) as string;
// console.log(data)
const data = cache.resolve(key, "posts") as string[];
const _hasMore = cache.resolve(key, "hasMore");
if (!_hasMore) {
hasMore = _hasMore as boolean;
}
results.push(...data);
// console.log(data, hasMore);
});
// return results;
// we need to shape our return object as below
// also we need to add the __typename property with the value as ObjectType name
return {
__typename: "PaginatedPosts",
posts: results,
hasMore: true,
};
}
You will have to face this error also.
Invalid key: The GraphQL query at the field at `Query.posts({"limit":10})` has a selection set, but no key could be generated for the data at this field.
You have to request `id` or `_id` fields for all selection sets or create a custom `keys` config for `PaginatedPosts`.
Entities without keys will be embedded directly on the parent entity. If this is intentional, create a `keys` config for `PaginatedPosts` that always returns null.
(Caused At: "Posts" query)
To avoid that we need to define the key property in cacheExchanger
. Add the below code to createUrqlClient
.
cacheExchange({
keys: {
PaginatedPosts: () => null,
},
Now only remaining to change the “load more” button logic to show and hide it.
{ data && data.posts.hasMore ? ( // because data's shape ObjectType shape
<Flex>
<Button
onClick={() => {
setVariables({
limit: variables.limit,
cursor: data.posts.posts[data.posts.posts.length - 1].createdAt,
});
}}
m="auto"
my={8}
isLoading={fetching}
>
load more
</Button>
</Flex>
) : null
}
That’s it. Now we added all the logic for the pagination.
Thanks for reading this. If you have anything to ask regarding this please leave a comment here. Also, I wrote this according to my understanding. So if any point is wrong, don’t hesitate to correct me. I really appreciate you. That’s for today friends. See you soon. Thank you.
References:
This article series based on the Ben Award - Fullstack React GraphQL TypeScript Tutorial. This is amazing tutorial and I highly recommend you to check that out.
Main image credit