w3resource

Paginated queries


Paginated queries

In the previous tutorial, we talked about resolvers, from which we ran our queries. We observed that on running the launches query, a large data set of launches were returned, which can slow down our app. Now the challenge is, how can we ensure we're not fetching too much data at once?

Pagination!!! This is a solution that ensures that the server only sends data in small chunks. We will be using cursor-based pagination approach in this tutorial over numbered pages approach, because it eliminates the possibility of skipping items and displaying the same item more than once.

In cursor-based pagination, a constant pointer (or cursor) is used to keep track of where in the data set the next items should be fetched from to avoid duplication.

To implement cursor-based pagination in our graph API. Open up the src/schema.js file and update the Query type with launches and also add a new type called LaunchConnection to the schema as shown below:

// src/schema.js
type Query {
  launches( 
    pageSize: Int
    after: String
  ): LaunchConnection!
  launch(id: ID!): Launch
  me: User
}
type LaunchConnection { 
  cursor: String!
  hasMore: Boolean!
  launches: [Launch]!
}

Now, the launches query will take two parameters, pageSize and after, and returns a LaunchConnection. The LaunchConnection type returns a result that shows the list of launches, in addition to a cursor field that keeps track of where we are in the list and a hasMore field to indicate if there's more data to be fetched.

After the above steps, open the src/utils.js and check out the paginateResults function. The paginateResults function in the file is a helper function for paginating data from the server.

Having doe this, we will update the necessary resolver functions to accommodate pagination.

Start by importing paginateResults and replacing the launches resolver function in the src/resolvers.js file with the code below:

//src/resolvers.js
const {paginateResults} = require('./utils');
module.exports = {
  Query: {
    launches: async (_, {pageSize = 20, after}, { dataSources }) => {
      const allLaunches = await dataSources.launchAPI.getAllLaunches();
      allLaunches.reverse();
      const launches = paginateResults({
        after,
        pageSize,
        results: allLaunches
      });
      return {
        launches,
        cursor: launches.length ? launches[launches.length - 1].cursor : null,
        hasMore: launches.length
          ? launches[launches.length - 1].cursor !==
            allLaunches[allLaunches.length - 1].cursor
          : false
      };
    },
    launch: (_, {id}, {dataSources}) =>
      dataSources.launchAPI.getLaunchById({launchId:id}),
     me: async (_, __, {dataSources}) =>
      dataSources.userAPI.findOrCreateUser(),
  }
};

Write resolvers on types

GraphQL is so flexible to the extent that we can write resolvers for any types in our schema, not just queries and mutations. This should be noted.

You may have noticed that we haven't written resolvers for all our types, yet our queries still run successfully. GraphQL has default resolvers; therefore, we don't have to write a resolver for a field if the parent object has a property with the same name.

Let's look at a case where we do want to write a resolver on our Mission type. Navigate to src/resolvers.js and copy this resolver into our resolver map underneath the Query property:

//src/resolvers.js
Mission: {
  missionPatch: (mission, { size } = { size: 'LARGE' }) => {
    return size === 'SMALL'
      ? mission.missionPatchSmall
      : mission.missionPatchLarge;
  },
},

//src/schema.js
type Mission{
   missionPatch(mission: String, size: PatchSize): String
}

The first argument passed into our resolver is the parent, which refers to the mission object. The second argument is the size we pass to our missionPatch field, which we use to determine which property on the mission object we want our field to resolve to.

Now that we know how to add resolvers on types other than Query and Mission, let's add some more resolvers to the Launch and User types. Copy this code into your resolver map:

//src/resolvers.js
Launch: {
  isBooked: async (launch, _, { dataSources }) =>
    dataSources.userAPI.isBookedOnLaunch({ launchId: launch.id }),
},
User: {
  trips: async (_, __, { dataSources }) => {
  const launchIds = awaitdataSources.userAPI.getLaunchIdsByUser();
    if (!launchIds.length) return [];
    return (
      dataSources.launchAPI.getLaunchesByIds({
        launchIds,
      }) || []
    );
  },
},

You may be amazed, thinking where we're getting the user from in order to fetch their booked launches. This is a great observation! we still need to authenticate our users and we will do that in the very next tutorial

Previous: Write your graph's resolvers
Next: Run your graph in production