GraphQL

How to Leverage Apollo Client Fetch Policies Like the Pros

Dustin Goodman
7 min read
Dustin - How to Leverage Apollo Client Fetch Policies Like the Pros
This article was written over 18 months ago and may contain information that is out of date. Some content may still be relevant, but please refer to official documentation for the latest information.

Apollo Client provides a rich ecosystem and cache for interfacing with your GraphQL APIs. You write your query and leverage the useQuery hook to fetch your data. It provides you with some state context and eventually resolves your query. That data is stored in a local, normalized, in-memory cache, which allows Apollo Client to respond to most previously run requests near instantaneously. This has huge benefits for client performance and the feel of your apps. However, sometimes Apollo's default doesn't match the user experience you want to provide. They provide fetch policies to allow you to control this behavior on each query you execute. In this article, we'll explore the different fetch policies and how you should leverage them in your application.

cache-first

This is the default for Apollo Client. Apollo will execute your query against the cache. If the cache can fully fulfill the request, then that's it, and we return to the client. If it can only partially match your request or cannot find any of the related data, the query will be run against your GraphQL server. The response is cached for the next query and returned to the handler.

This method prioritizes minimizing the number of requests sent to your server. However, it has an adverse effect on data that changes regularly. Think of your social media feeds - they typically contain constantly changing information as new posts are generated. Or a real-time dashboard app tracking data as it moves through a system. cache-first is probably not the best policy for these use cases as you won't fetch the latest data from the upstream source. You can lower the cache time of items for the dashboard to avoid the staleness issue and still minimize the requests being made, but this problem will persist for social media feeds.

The cache-first policy should be considered for data that does not change often in your system or data that the current user fully controls. Data that doesn't change often is easily cached, and that's a recommended pattern. For data that the user controls, we need to consider how that data changes. If only the current user can change it, we have 2 options: Return the updated data in the response of any mutation which is used to update the cache Use cache invalidation methods like refetchQueries or onQueryUpdated These methods will ensure that our cache stays in sync with our server allowing the policy to work optimally. However, if other users in the system can make changes that impact the current user's view, then we can not invalidate the cache properly using these strategies which makes this policy unideal.

network-only

This policy skips the cache lookup and goes to the server to fetch the results. The results are stored in the cache for other operations to leverage. Going back to the example I gave in my explanation cache-first of a social media feed, the network-only policy would be a great way to implement the feed itself as it's ever-changing, and we'll likely even want to poll for changes every 10s or so. The following is an example of what this component could look like:

import { gql, useQuery } from '@apollo/client';

const GET_FEED = gql`
  query GetFeed {
    feed {
      nodes {
        id
        post
        createdAt
        author {
          id
          # ...
        }
        interactions {
          likes {
            totalCount
          }
          reposts {
            totalCount
          }
        }
      }
      paginationInfo {
        hasNextPage
        nextCursor
      }
    }
  }
`;

export default function SocialFeed() {
  const { loading, error, data } = useQuery(GET_FEED, {
    fetchPolicy: 'network-only', // only fetch from our GraphQL server
    pollInterval: 10 * 1000, // refetch the feed every 10 seconds
  });

  if (loading) return 'Loading...';
  if (error) return `Error! ${error.message}`;

  const { feed } = data;
  const { nodes: posts, paginationInfo } = feed;
  const { hasNextPage, nextCursor } = paginationInfo;

  return (
    <>
      <Feed posts={posts} />
      {hasNextPage && (<GetMorePosts cursor={nextCursor} />)}
    </>
  );
}

Whenever this SocialFeed component is rendered, we always fetch the latest results from the GraphQL server ensuring we're looking at the current data. The results are put in the cache which we can leverage in some children components.

cache-only

cache-only only checks the cache for the requested data and never hits the server. It throws an error if the specified cache items cannot be found. At first glance, this cache policy may seem unhelpful because it's unclear if our cache is seeded with our data. However, in combination with the network-only policy above, this policy becomes helpful. This policy is meant for components down tree from network-only level query. This method is for you if you're a fan of React components' compatibility. We can modify the return of our previous example to be as follows:

return (
    <>
      {posts.map(post => (
        <Post key={post.id} postId={post.id} />
      ))}
      {hasNextPage && (<GetMorePosts cursor={nextCursor} />)}
    </>
  )

Notice we're not passing the full post object as a prop. This simplifies our Post component types and makes later refactors easier. The Post would like like the following:

export const POST_FRAGMENT = gql`
  fragment PostFragment on Post {
    id
    post
    createdAt
    author {
      id
      # ...
    }
    interactions {
      likes {
        totalCount
      }
      reposts {
        totalCount
      }
    }
  }
`;

const GET_POST = gql`
  query GetPost($postId: ID!) {
    post(id: $postId) {
      ...PostFragment
    }
  }
`;

export function Post({ postId }) {
  const { data } = useQuery(GET_POST, {
    variables: { postId },
    fetchPolicy: 'cache-only',
  });

  const { post } = data;

  return (
    <div>
      <p>{post.post}</p>
      <p>{post.interactions.likes.totalCount} likes</p>
      <p>{post.interactions.reposts.totalCount} reposts</p>
    </div>
  )
}

In this query, we're grabbing the data directly from our cache every time because our top-level query should have fetched it. Now, a small bug here makes maintainability a bit harder. Our top-level GetFeed query doesn't guarantee fetching the same fields. Notice how our Post component exports a fragment. Fragments are a feature Apollo supports to share query elements across operations. In our SocialFeed component, we can change our query to be:

const GET_FEED = gql`
  query GetFeed {
    feed {
      nodes {
        …PostFragment
      }
      paginationInfo {
        hasNextPage
        nextCursor
      }
    }
  }
`;

Now, as we change our Post to use new fields and display different data, the refactoring is restricted to just that component, and the upstream components will detect the changes and handle them for us making our codebase more maintainable. Because the upstream component is always fetching from the network, we can trust that the cache will have our data, making this component safe to render. With these examples, though, our users will likely have to see a loading spinner or state on every render unless we add some server rendering.

cache-and-network

This is where cache-and-network comes to play. With this policy, Apollo Client will run your query against your cache and your GraphQL server. This further simplifies our example above if we want to provide the last fetched results to the user but then update the feed immediately upon gathering the latest data. This is similar to what X/Twitter does when you reload the app. You'll see the last value that was in the cache then it'll render the network values when ready. This can cause a jarring user experience though, if the data is changing a lot over time, so I recommend using this methodology sparsely. However, if you wanted to update our existing example, we'd just change our SocialFeed component to use this policy, and that'll keep our client and server better in sync while still enabling 10s polling.

no-cache

This policy is very similar to the network-only policy, except it bypasses the local cache entirely. In our previous example, we wrote engagement as a sub-selector on a Post and stored fields there. These metrics can change in real time pretty drastically. Chat features, reactions, viewership numbers, etc., are all types of data that may change in real time. The no-cache policy is good when this type of data is active, such as during a live stream or within the first few hours of a post going out. You may typically want to use the cache-and-network policy eventually but during that active period, you'll probably want to use no-cache so your consumers can trust your data. I'd probably recommend changing your server to split these queries and run different policies for the operations for performance reasons. I haven't mentioned this yet, but you can make the fetch policy on a query dynamic, meaning you combine these different policies' pending states. This could look like the following:

import { gql, useQuery } from '@apollo/client';

const GET_CHAT = gql`
  query GetChat {
    chat {
      id
      messages {
        id
        content
        author {
          id
          name
        }
      }
    }
  }
`;

export default function ChatFeed({ isLive }: { isLive: boolean }) {
  const { loading, error, data } = useQuery(GET_CHAT, {
    fetchPolicy: isLive ? 'no-cache' : 'cache-and-network', // only cache if we're not live
    pollInterval: 10 * 1000, // refetch the feed every 10 seconds
  });

  if (loading) return 'Loading...';
  if (error) return `Error! ${error.message}`;

  const { chat } = data;
  const { messages } = chat;

  return (
    <Chat messages={messages} />
  );
}

We pass whether the event is live to the component that then leverages that info to determine if we should cache or not when fetching the chat. That being said, we should consider using subscription operations for this type of feature as well, but that's an exercise for another blog post.

standby

This is the most uncommon fetch policy, but has a lot of use. This option runs like a cache-first query when it executes. However, by default, this query does not run and is treated like a "skip" until it is manually triggered by a refetch or updateQueries caller. You can achieve similar results by leveraging the useLazyQuery operator, but this maintains the same behavior as other useQuery operators so you'll have more consistency among your components. This method is primarily used for operations pending other queries to finish or when you want to trigger the caller on a mutation. Think about a dashboard with many filters that need to be applied before your query executes. The standby fetch policy can wait until the user hits the Apply or Submit button to execute the operation then calls a await client.refetchQueries({ include: ["DashboardQuery"] }), which will then allow your component to pull in the parameters for your operation and execute it. Again, you could achieve this with useLazyQuery so it's really up to you and your team how you want to approach this problem. To avoid learning 2 ways, though, I recommend picking just one path.

Conclusion

Apollo Client's fetch policies are a versatile and helpful tool for managing your application data and keeping it in sync with your GraphQL server. In general, you should use the defaults provided by the library, but think about the user experience you want to provide. This will help you determine which policy best meets your needs. Leveraging tools like fragments will enable you to manage your application and use composable patterns more effectively.

With the rise of React Server Components and other similar patterns, you'll need to be wary of how that impacts your Apollo Client strategy. However, if you're on a legacy application that leverages traditional SSR patterns, Apollo allows you to pre-render queries on the server and their related cache. When you combine these technologies, you'll find that your apps perform great, and your users will be delighted.

About the author

Dustin Goodman

Dustin Goodman

Engineer Manager

Engineering Manager with a passion for web and application development. Speaker and writer on work experiences and software development. Dog dad and musician fueled by coffee.

Keep reading

View all posts →

How to create and use custom GraphQL Scalars

Enter the world of custom GraphQL scalars. These data types go beyond the conventional, offering the power and flexibility to tailor your schema to precisely match your application's unique needs. ...

William Mimura5 mins
GraphQLJavaScript

Efficiently Extract Object References in Shopify Storefront GraphQL API

Dive into the intricacies of Shopify's Storefront API with our blog post, 'Efficiently Extract Object References in Shopify Storefront GraphQL API'. Discover a hands-on guide to extracting complex data types, like images, from metafields and metaobjects....

William Mimura4 mins
GraphQLShopify

Leveraging GraphQL Scalars to Enhance Your Schema

Explore the transformative power of GraphQL scalars with the graphql-scalars library, designed to extend the default scalar types in GraphQL schemas. ...

William Mimura4 mins
GraphQL

Introducing the New Serverless, GraphQL, Apollo Server, and Contentful Starter kit

Introducing the new Serverless, GraphQL, Apollo Server, and Contentful Starter kit The team at This Dot Labs has released a brand new starter kit which includes the Serverless Framework, GraphQL, Apollo Server and Contentful configured and ready to go. This article will walk through how to set up the new kit, the key technologies used, and reasons why you would consider using this kit. Table of Contents How to get started setting up the kit Generate the project Setup Contentful access Setting up Docker Starting the local server How to Create the Technology Model in Contentful How to seed the database with demo data How to work with the migration scripts Technologies included in this starter kit Why use GraphQL? Why use Contentful? Why use Amazon Simple Queue Service (SQS)? Why use Apollo Server? Why use the Serverless Framework? Why use Redis? Why use the Jest testing framework? Project structure How to deploy your application What can this starter kit be used for? Conclusion How to get started setting up the kit Generate the project In the command line, you will need to start the starter.dev CLI by running the npx @this dot/create starter command. You can then select the Serverless Framework, Apollo Server, and Contentful CMS kit and name your new project. Then you will need to cd into your new project directory and install the dependencies using the tool of your choice (npm, yarn, or pnpm). Next, you will need to Run cp .env.example .env to copy the contents of the .env.example file into the .env file. Setup Contentful access You will first need to create an account on Contentful, if you don't have one already. Once you are logged in, you will need to create a new space. From there, go to Settings API keys and click on the Content Management Tokens tab. Next, click on the Generate personal token button and give your token a name. Copy your new Personal Access Token, and add it to the CONTENTFUL CONTENT MANAGEMENT API TOKEN variable. Then, go to Settings General settings to get the CONTENTFUL SPACE ID. The last step is to add those CONTENTFUL CONTENT MANAGEMENT API TOKEN and CONTENTFUL SPACE ID values to your .env file. Setting up Docker You will first need to install Docker Desktop if you don't have it installed already. Once installed, you can start up the Docker container with the npm run infrastructure:up command. Starting the local server While the Docker container is running, open up a new tab in the terminal and run npm run dev to start the development server. Open your browser to http://localhost:3000/dev/graphql to open up Apollo server. How to Create the Technology Model in Contentful To get started with the example model, you will first need to create the model in Contentful. 1. Log into your Contentful account 2. Click on the Content Model tab 3. Click on the Design your Content Modal button if this is your first modal 4. Create a new model called Technology 5. Add three new text fields called displayName, description and url 6. Save your new model How to seed the database with demo data This starter kit comes with a seeding script that pre populates data for the Technology Content type. In the command line, run npm run db:seed which will add three new data entries into Contentful. If you want to see the results from seeding the database, you can execute a small GraphQL query using Apollo server. First, make sure Docker, and the local server(npm run dev) are running, and then navigate to http://localhost:3000/dev/graphql. Add the following query: When you run the query, you should see the following output. How to work with the migration scripts Migrations are a way to make changes to your content models and entries. This starter kit comes with a couple of migration scripts that you can study and run to make changes to the demo Technology model. These migration scripts are located in the scripts/migration directory. To get started, you will need to first install the contentful cli. You can then login to Contentful using the contentful cli. You will then need to choose the Contentful space where the Technology model is located. If you want to modify the existing demo content type, you can run the second migration script from the starter kit. If you want to build out more content models using the CLI, you can study the example code in the /scripts/migrations/01 create technology contentType.js file. From there, you can create a new migration file, and run the above contentful space migration command. If you want to learn more about migration in Contentful, then please check out the documentation. Technologies included in this starter kit Why use GraphQL? GraphQL is a query language for your API and it makes it easy to query all of the data you need in a single request. This starter kit uses GraphQL to query the data from our Contentful space. Why use Contentful? Contentful is a headless CMS that makes it easy to create and manage structured data. We have integrated Contentful into this starter kit to make it easy for you to create new entries in the database. Why use Amazon Simple Queue Service (SQS)? Amazon Simple Queue Service (SQS) is a queuing service that allows you to decouple your components and process and store messages in a scalable way. In this starter kit, an SQS message is sent by the APIGatewayProxyHandler using the sendMessage function, which is then stored in a queue called DemoJobQueue. The SQS handler sqs handler polls this queue, and processes any message received. Why use Apollo Server? Apollo Server is a production ready GraphQL server that works with any GraphQL client, and data source. When you run npm run dev and open the browser to http://localhost:3000/dev/graphql, you will be able to start querying your Contentful data in no time. Why use the Serverless Framework? The Serverless Framework is used to help auto scale your application by using AWS Lambda functions. In the starter kit, you will find a serverless.yml file, which acts as a configuration for the CLI and allows you to deploy your code to your chosen provider. This starter kit also includes the following plugins: serverless offline allows us to deploy our application locally to speed up development cycles. serverless plugin typescript allows the use of TypeScript with zero config. serverless dotenv plugin preloads function environment variables into the Serverless Framework. Why use Redis? Redis is an open source in memory data store that stores data in the server memory. This starter kit uses Redis to cache the data to reduce the API response times and rate limiting. When you make a new request, those new requests will be retrieved from the Redis cache. Why use the Jest testing framework? Jest is a popular testing framework that works well for creating unit tests. You can see some example test files under the src/schema/technology directory. You can use the npm run test command to run all of the tests. Project structure Inside the src directory, you will find the following structure: This given structure makes it easy to find all of the code and tests related to that specific component. This structure also follows the single responsibility principle which means that each file has a single purpose. How to deploy your application The Serverless Framework needs access to your cloud provider account so that it can create and manage resources on your behalf. You can follow the guide to get started. Steps to get started: 1. Sign up for an AWS account 2. Create an IAM User and Access Key 3. Export your AWS ACCESS KEY ID & AWS SECRET ACCESS KEY credentials. 4. Deploy your application on AWS Lambda: 5. To deploy a single function, run: To stop your Serverless application, run: For more information on Serverless deployment, check out this article. What can this starter kit be used for? This starter kit is very versatile, and can be used with a front end application for a variety of situations. Here are some examples: personal developer blog small e commerce application Conclusion In this article, we looked at how we can get started using the Serverless, GraphQL, Apollo Server, and Contentful Starter kit. We also looked at the different technologies used in the kit, and why they were chosen. Lastly, we looked at how to deploy our application using AWS. I hope you enjoy working with our new starter kit!...

Jessica Wilkins7 mins
GraphQL