DEV Community

loading...

Apollo Client fetchPolicies, React, and Pre-Rendering

hotgazpacho profile image Will Green ・4 min read

Background

My team at FireEye built the FireEye Market as a React app with a GraphQL (Apollo Server Lambda) backend. It's a marketplace to "Discover apps, extensions, and add-ons that integrate with and extend your FireEye experience." One of the things we discovered early on was that we needed to work to improve the Time to First Meaning Paint (TTFMP). We couldn't really reduce our bundle size further, and we already do code splitting. So we looked instead to generate static HTML, with the Apollo Client cache data serialized into the markup. This allows the client to quickly download a fully rendered HTML page, to begin interacting with immediately, while the React app scripts are downloaded and evaluated by the browser. When the React app hydrates, it has been configured to read the serialized data into the Apollo Client cache, which then makes the data instantly available to the React app to update the component tree. However, there is a catch...

Enter fetchPolicy

Apollo Client, and the corresponding React components (Query, Mutation, Subscription, and graphql HOC that encapsulates them) that consume the client, have an option called fetchPolicy. What this does is control how the components interact with the Apollo Client cache. This is very powerful, but the documentation for it is spread out in a couple places in the Apollo docs. My aim here is to consolidate that information, and hopefully, clarify it a bit.

The valid values for cachePolicy are:

cache-first

This is the default if you don't explicitly specify an option. What this means is that the client will look in its cache, and if it finds all of the data it needs to fulfill the query, it will use that and not make a network request for the data. Each of the queries you make, along with the arguments, are stored in the cache. If the query is cached, then it will use the data from this query. I believe that the selection set of the query is also considered, so if that differs, a network request will be made.

I'm admittedly unsure on this last point. The FireEye Market app has a known set of queries that the client executes, which differ only in the parameters passed at runtime.

cache-and-network

This policy will look in the cache first, and use that data if available. It will always make a network request, updating the cache and returning the fresh data when available. This may result in an additional update to your components when the fresh data comes in. This policy optimizes for getting cached data to the client quickly, while also ensuring that fresh data is always fetched.

This is the policy that we have found to work best for most cases when dealing with pre-rendering.

network-only

This policy skips reading from the cache altogether and goes straight to the network for data. Queries using this option will never read from the cache. It will, however, write the results to the cache. This is for the situation where you always want to go to the backend for data, and are willing to pay for it in response time.

cache-only

This policy exclusively reads from the cache, and will never go to network. If the data doesn't exist in the cache, then an error is thrown. This is useful for scenarios where you want the client to operate in offline mode only, where the entirety of the data exists on the client.

I've never used this policy myself, so take that assertion with a giant grain of salt.

no-cache

This policy will never read data from, nor write data to, the cache.

Configuration

Armed with this knowledge of fetchPolicy, how do you configure it? There's two places: in the client config, and in the request config.

Client Configuration

When you configure the Apollo Client instance, you can provide it with a defaultOptions key, which specifies the policy each type of query should use unless specifically provided by the request.

const defaultOptions = {
  watchQuery: {
    fetchPolicy: 'cache-and-network',
    errorPolicy: 'ignore',
  },
  query: {
    fetchPolicy: 'network-only',
    errorPolicy: 'all',
  },
  mutate: {
    errorPolicy: 'all'
  }
}

From the docs:

Note: The React Apollo <Query /> component uses Apollo Client's watchQuery functionality, so if you would like to set defaultOptions when using <Query />, be sure to set them under the defaultOptions.watchQuery property.

Also note that the graphql HOC, which given a document that is a query, ends up wrapping an instance of the <Query /> component.

Request Configuration

You can also specify the fetchPolicy per request. One of the props that you can provide to the <Query /> component is fetchPolicy. This will override whatever is configured in the client for this query only.

<Query query={QUERY_DOCUMENT} fetchPolicy="network-only">
  {(data) => { /* render prop! */ }}
</Query>

Similarly for the graphql HOC, you can specify a fetchPolicy in the config object:

const listAppsForNotificatonSettings = graphql(APPS_FOR_NOTIFICATION_SETTINGS_QUERY, {
  options: {
    fetchPolicy: 'cache-first' 
  }
});

Conclusion

As I mentioned, we found that this cache-and-network policy ended up being the best option for providing the best experience for our customers when serving up pre-rendered pages for various entry points into the application. In a few cases, we found that using cache-first was a better option, but this are few. As always, this is what worked for my team. Your mileage may vary.

Discussion

pic
Editor guide