A recipe for offline support in React Apollo
React Apollo is undoubtedly the current best choice for integrating your React applications with GraphQL APIs. Using Apollo client gives you declarative data fetching, zero-config caching, easy combination of local and remote data, and a vibrant ecosystem. So what could be missing?
Offline support! Building offline support into Javascript applications has never been easy, except for offline-first tooling like CouchDB. Although Apollo makes some steps in the right direction for true offline support, it still has a way to go in some aspects. This tutorial will help guide you through what you need to get your application offline.
Let’s look at what we can do to offer our users better offline support and better robustness to transient networks!
Offline queries
The first ingredient for offline support is a feature integrated deeply into Apollo. Apollo provides it automatically for you by default, so it’s almost not worth discussing. That solution is the cache!
When the result of a GraphQL query comes back, Apollo stores it in the cache. By default, Apollo will pull results from the cache when a request comes up again, saving you the need to make another request. You can, of course, force it to do so with polling or refetching, but in our case we’ll want to take as much advantage of the cache as possible.
When the network is unavailable, neither the user nor your code may even notice as long as results for their queries are in the cache. This forms the backbone of our offline support, so remember how it works!
Offline mutations
Optimistic UI is another component integrated deeply into Apollo that you’ll want to make sure you are leveraging. Optimistic responses allow you to provide an ideal version of what the server will return, before it returns it. This has performance benefits even when online, as it gives users a near instantaneous UI updates for mutations. Here is what one looks like:
When you perform your mutation, Apollo will run the update and cache integration as if the optimistic response was the server’s response. When the actual response comes back, Apollo will overwrite the optimistic changes in the cache with the real result.
This feature enables the write-based offline functionality we’ll want our users to have. When the server is unavailable and can’t respond, we still have a mechanism for moving logic forward in the application and changing the cache state.
You should make liberal usage of update functions in your application to take advantage of this. Some mutations Apollo can infer how to update the cache based on the response, but for some it can’t. Especially in an offline situation, where you can’t refresh stale data, you should use update functions to implement your non-obvious changes to the cache.
Another issue is that, by default, when the server is unavailable GraphQL will roll back the optimistic UI with the result of the permanently failed request. In most cases, if our server disconnection is temporary, we’d prefer to continue on with the optimistic result until we can be connected again. This is where we’ll use apollo-link-retry, a package in the ecosystem that fits between the Apollo client and the HttpLink you use to talk to your GraphQL endpoint:
When RetryLink sees that the request to HttpLink (or whichever other link you use to talk to your server) failed due to network issues, it will not pass along the error response but instead wait and retry the request again. As configured here, it does this until the request succeeds. This means that, for a disconnected network of an offline user, the optimistic UI will remain in place, and it will appear that their operation has succeeded until a time they can connect to the internet. When they do, the backlog of actions they performed will be sent to the server by RetryLink and the real cache changes will replace the optimistic ones.
Queries across refreshes
Up to here we’ve built a nice UI for handling temporary losses of internet connectivity. However, a true offline application can handle opening and running of the application with no internet at all (or across refreshes). In the current state, if the user was to refresh their application all of their queued mutations, stored cache data, and the application itself would be lost. Instead, they’d see a nice “you are not connected to the internet” error message.
The first piece of this — ensuring that the application can be served even when there is no internet — is not part of Apollo but part of service workers. You’ll want to utilize a service worker to store the latest version of your application, and serve it to the client when the internet isn’t present. We won’t go into that here, because there are quite a few tutorials that cover it, but we’ll recommend this tutorial on MDN.
With the service worker in place, your application will be able to start, but it will have no cached data to show the user! What we want to do is sync the Apollo cache to a persistent storage, such as local storage, so it can persist across refreshes. While being handy for offline support, this also has performance benefits when your app is online as it reduces the number of requests the client has to make. To do this we’ll use apollo-cache-persist, another plugin from the Apollo ecosystem.
The trick with apollo-cache-persist is to be sure that you have restored the results from local storage, and repopulated the cache, before you let anything in your app start up that depends on it. We’ll update our startup code to look like this:
Mutations across refreshes
This approach works for the queries in the application, but what about mutations? Our handy combination of optimistic UI, update functions, and apollo-link-retry keep mutations queued smoothly during sessions. However, over a page refresh or fresh start of the application these will be lost and instead users will see the most recent version of the application’s cache (without any of the optimistic updates applied).
This is one that Apollo doesn’t have a great plugin or solution for quite yet. However, we can code our own implementation that provides some resemblance of this feature!
To do this, we’ll replace the default Apollo mutation with our own custom OfflineMutation component that looks like the following:
This custom component behaves just like a normal Mutation, but it serializes the mutation and stores it into the browser’s local storage. When the request comes back successfully, it removes it from the local storage. Your components stay the same, but use OfflineMutation instead.
Any requests that don’t complete will be serialized in local storage. We’ll use this function to get them out of local storage and running again at startup:
When the browser starts up again fresh, we can call the function to make sure that the stored mutations are started again:
The one caveat here: You can’t serialize update functions! Because it’s a function, there is no way to convert it to a JSON string that we can restore later. Although we could in theory cast the function to a string and then eval() it to load it again, the dependencies or other logic in the application might not be the same between refreshes so this has some drawbacks.
Instead, what we do here is overwrite the update function with a custom update function. It has some of its own logic to do – such as deleting the returned request from the local storage store – but it will call a custom function called updateHandler that will decide which update function to call corresponding to the incoming response. This reverses the responsibilities slightly from the traditional Apollo model for components with update functions — rather than telling the mutation component what your update function is going to be, your update function needs to have a layer of indirection — but it allows us to get around the issues with serialization.
This approach has a word of warning: your business logic has to be aware of this! You do have to consider that, if you change your request format drastically, your clients may have cached serialized mutations that have the parameters and optimistic responses of an older version. Your app has to handle this potential for stale data and requests.
Conclusion
The zero-config cache and optimistic UI provide huge performance and responsiveness benefits, but also form a framework for strong offline support. We can extend these tools with plugins from the Apollo ecosystem like apollo-link-retry to get better coverage in the event of a transient outage, and apollo-cache-persist to get more robust data retrieval across refreshes. Lastly, we can branch out of the ecosystem a bit and use some custom code to get stable mutations across page refreshes in an offline ecosystem.
Using these approaches, paired with service workers to serve the application and some thoughtful business logic design, your application can be robust to even lengthy usage without internet access.
Are you exploring Apollo, GraphQL, or React? We consult on modern web technologies with organizations in all different industries, and we can help orientate your technical architecture for the future. Drop us a line!

