Codiga has joined Datadog!

Read the Blog·

Interested in our Static Analysis?

Sign up
← All posts
Oscar Salazar Saturday, July 2, 2022

GraphQL nested pagination with Apollo Client and React

Share

AUTHOR

Oscar Salazar, Senior Software Engineer

Oscar is a Software Engineer passionate about frontend development and creative coding, he has worked in several projects involving from video games to rich interactive experiences in different web applications. He loves studying and playing with the newest CSS features to create fantastic art.

See all articles

Apollo Client nested pagination

As developers sooner or later we are going to have to put in place a paginated list of results. In this post we are going to explore how to implement pagination with Apollo Client.

Pagination in GraphQL can be tricky, this is why the team behind Apollo Client created a nice API to deal with it. Lets start by understanding the basic concepts.

Cache

When we use Apollo Client we need to create a cache where the results of our GraphQL queries will be stored. The Apollo cache is nothing but a normalized in memory data store. Each field you retrieve from your queries will end up in a map like data structure with a unique identifier.

The cache mechanism is a very efficient way to optimize our application data needs. Since our data might be already present in cache, Apollo can answer almost immediately to a query. We will only need to do a round trip to our servers during the initial request or if we want to refresh our data.

Apollo Client will even use the cache in different queries if the same field is requested. It's quite amazing considering you almost never need to deal with it.

If you want to dive deeper I recommend you to head to the official Apollo Caching documentation here.

Filed policies

With a field policy we can tell Apollo how to read and write fields to the cache. This will help us merge our upcoming page data and read the results as if they were from the same query.

Field policies also let us define a set of key arguments to tell the cache when to create a new set of results. For example when we have filters or order arguments, in this case we most likely want to create a new list.

For more advanced topics head to the official field policies documentation.

API

It doesn't matter if we have a cursor-based or offset-based pagination, Apollo exposes a common API. All we need to do is have a clear understanding of the cache and field policies.

Every time we create a query Apollo gives us a fetchMore function. This function will help us retrieve more pages and we will use the field policies to store the new data.

You can read the official Apollo Client pagination API here, it's very well documented.

Implementation

Let's start by defining our query.

const PAGINATED_QUERY = gql`  
 query Example(  
   $howmany: Long!,  
   $skip: Long!,  
   $desc: Boolean) {  
   $nestedSkip: Long!,  
   $nestedDesc: Boolean  
 ) {  
   projects(howmany: $howmany, skip: $skip, desc: $desc) {  
     id  
     name  
     nested(howmany: $howmany, skip: $nestedSkip, desc: $nestedDesc) {  
       id  
       name  
     }  
   }  
 }  
`;

As you can see we are using a nested field within our initial query. We will explore how to set up our field policies to support this case.

Now that we have our query defined we can use it in our components.

const MyComponent = ({ howmany }) => {
  const { data, loading, error, fetchMore } = useQuery(PAGINATED_QUERY, {
    variables: { howmany, skip: 0, nestedSkip: 0 },
  });

  const onNewPage = (page) => {
    fetchMore({ variables: { skip: page * howmany } });
  };

  const onNewNestedPage = (page) => {
    fetchMore({ variables: { nestedSkip: page * howmany } });
  };
};

Here when our component is mounted we will execute a query to retrieve our data. The initial page is 0 so we are not going to skip any results.

When our page change whether is the main field or the nested one, we will use the fetchMore function to bring more results.

Now all we need to do is tell Apollo Client how to merge all the pages together per query. Lets start by defining the main pagination policy.

new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        projects: skipLimitPagination(["desc", "howmany"]),
      },
    },
  },
});

When we create our cache instance we are going to use the Query field policy. This policy states that our data should be merged using an offset-based pagination. In our case is called skip-limit due to our API, but it doesn't matter, adjust as you need.

Next lets defined our nested pagination policy.

new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        projects: skipLimitPagination(["desc", "howmany"]),
      },
    },
    Project: {
      fields: {
        nested: skipLimitPagination(["desc", "howmany"]),
      },
    },
  },
});

This time we will define our policy in the Project type instead of the Query. This way we can support nested paginations and each one will have independent data.

One thing to notice is the skipLimitPagination function, here is the implementation.

function skipLimitPagination(keyArgs = false) {
  return {
    keyArgs,
    merge(existing, incoming, { args }) {
      const merged = existing ? existing.slice(0) : [];

      if (args) {
        // Assume an offset of 0 if args.offset omitted.
        const { skip = 0 } = args;
        for (let i = 0; i < incoming.length; ++i) {
          merged[skip + i] = incoming[i];
        }
      } else {
        // It's unusual (probably a mistake) for a paginated field not
        // to receive any arguments
        throw new Error("No args provided to paginated field");
      }
      return merged;
    },
  };
}

This function receives a keyArgs param, it can be a falsy value or an array of query parameters that will force a new list of results. In this case we want our pagination to start over when the number of items change or the order.

The merge function receives the existing pages plus the new page. With this data all that's left to do is iterate over the incoming objects and append them to the existing ones in the proper order.

You can implement error handling based in your needs or any other complex merging logic here.

Our API in this example uses skip and howmany instead of the more common offset and limit pagination naming. If you use the common offset-limit naming conventions you can use Apollos utility function instead of having to implement your own.

import { offsetLimitPagination } from "@apollo/client/utilities";

Import the offsetLimitPagination limit function offered by the Apollo Client utilities and you are good to go.

Conclusion

Apollo Client offers a convenient set of method to deal with GraphQL pagination. You nest your queries any number you like and the Apollo cache would help you deal with pagination with ease.

Are you interested in Datadog Static Analysis?

Sign up