Highly Functional Offline Applications using Apollo Client
A concrete example using a familiar todos application.

The Problem
Create React Native (could be React Web too) application that is highly functional offline.
The Solution
This solution leverages the considerable power of Apollo Client to create a fairly declarative solution, i.e., more configuration and less code.
Also, this solution is inspired by another article written by Chris Foster; A recipe for offline support in React Apollo.
This solution is not appropriate for particularly large datasets (often read-only master data). For this situation, please refer to another article; Large Offline Datasets with Apollo Client.
The complete solution is available for download.
Offline Queries
The first part of the solution, as articulated by Chris, is the application’s ability to cache query results; something that is built into Apollo Client’s core functionality.
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.
— Chris Foster — A recipe for offline support in React Apollo
When the application first loads (when online) the GraphQL server’s result of the first execution of the allTodos query is cached in-memory. The results of subsequent executions are returned from that cached query.
Here pressing the Toggle Todos button, hiding and showing the todos, causes the application to re-execute the allTodos query; results are even shown when the application is offline.

Because this is Apollo Client’s default behavior, one only need to execute the query (without any special consideration) as seen in the application’s AppTodos component; src/components/App/AppTodos/index.tsx:
Offline Mutations
Mutations, on the other hand, do not work offline by default.
First, one can easily deal with transient network connectivity issues by incorporating the configurable apollo-link-retry link into the Apollo Client configuration; its default configuration is to try five attempts over a couple of seconds. We can see this configuration in the App component; src/components/App/index.tsx:
note: You might observe that the App component is fairly long (182 lines). This is because, all of the application’s loading configuration and logic are concentrated in this component.
Sidebar into Apollo Client Links
Before continuing, we need to better understand Apollo Client Links.
Apollo Link is designed to be a powerful way to compose actions around data handling with GraphQL. Each link represents a subset of functionality that can be composed with other links to create complex control flows of data.
— Apollo Team — Apollo Docs: Apollo Link: Concepts Overview

In the previous section, we see the application enhanced Apollo Client’s behavior with apollo-link-retry immediately before the terminating link, apollo-link-http.
Sidebar into Apollo Client Update Functions
Another particularly confusing aspect of Apollo Client is the use of update functions with mutations.
Sometimes when you perform a mutation, your GraphQL server and your Apollo cache become out of sync. This happens when the update you’re performing depends on data that is already in the cache; for example, deleting and adding items to a list. We need a way to tell Apollo Client to update the query for the list of items. This is where the update function comes in!
— Apollo Team — Apollo Docs: Apollo Client: Mutations
We can see that the application defines such update functions (handleCreateTodoUpdate and handleDeleteTodoUpdate in src/graphql/todos.ts).
For example, we see the use of the handleCreateTodoUpdate function when the application executes the createTodo mutation in the AppTodosCreate component; src/components/App/AppTodos/AppTodosCreate.tsx:
Offline Mutations (Continued)
When the application is known to be offline, it must not send mutations through to apollo-link-http (and thus the GraphQL server). The solution is to introduce another Apollo Client Link; apollo-link-queue.
An Apollo Link that acts as a gate and queues requests when the gate is closed. This can be used when there is no internet connection or when the user has explicitly set an app to offline mode.
— apollo-link-queue — apollo-link-queue
We can see how the application uses this link in the App component; src/components/App/index.tsx.
One non-obvious feature of Apollo Client is that, by default, queries and mutations are executed in parallel. While this feature is normally not a problem with online applications, it is problematic when using apollo-link-queue.
For example, say a user creates a todo and then subsequently deletes that same todo when offline. The apollo-link-queue link causes Apollo Client to queue the two mutations. When the application comes back online, both mutations are sent onto apollo-http-link (and the GraphQL server) in parallel. The delete todo mutation will likely fail as the create mutation likely would had not completed first. What to do?
You guessed it, the application uses another Apollo Client Link: apollo-link-serialize.
An Apollo Link that serializes requests by key, making sure that they execute in the exact order in which they were submitted.
— apollo-link-serialize — apollo-link-serialize
The application uses this link between apollo-link-queue and apollo-link-retry in the App component to ensure that mutations are sent to apollo-link-http (and the GraphQL server) in order; src/components/App/index.tsx.
In order for apollo-link-serialize to identify which mutations to serialize together, the application needs to add a key to the mutation’s context; in this case the application uses the same key, MUTATION, for all mutations, e.g., in src/components/App/AppTodos/AppTodosCreate.tsx:
Offline Mutations (But There is More)
As a reminder, to address offline mutations we have already introduced:
- apollo-link-retry: Deal with transient network connectivity issues
- apollo-link-queue: Gate mutations when offline
- apollo-link-serialize: Ordering mutations to guarantee sequential execution
But there is more…
When the application is online and the user creates a todo a couple of things happen; the application’s AppTodosComponent component:
- disables the input and button
- executes the mutation
- waits until the mutation resolves
- enables the input and button
- resets the form

note: In both the online and offline (below) scenarios, the mutation’s update function handles adding the todo to the cache and the reactive nature of the allTodos query causes the new todo to be displayed.
When the application is offline and the user creates a todo only two things happen; the application’s AppTodosComponent component:
- resets the form
- executes the mutation; providing an optimistic response. Notice that the todo’s title includes the string (PENDING)

note: Technically, the mutation also provides an optimistic response when online; however it updates to the actual response too quickly to see. Also, we will explore the Tracked Queries feature in a bit.
When the application comes back online, the queued mutation is executed and the actual response replaces the optimistic response. If, however, the queue mutation fails, the optimistic response is automatically removed from the cache. Now these are quite a nifty tricks.

All of this is implemented in the AppTodosCreate component; src/components/App/AppTodos/AppTodosCreate.tsx:
When online, mutation errors can be handled naturally in the user interface that created them, e.g., in the AppTodosCreate component. When coming back online, however, the mutations happen outside of any user interface. What to do?
You guessed it, the application uses another Apollo Client Link; apollo-link-error. With this in place, the application can appropriately handle errors, e.g., display a modal user interface. In this application, however, it only outputs to the console. This is setup in the App component; src/components/App/index.tsx
Queries Across Refreshes
So, what happens when the application loads when offline? Without additional precautions, the allTodos query is not cached in the in-memory cache and the query would fail.
Here the solution is not an Apollo Client Link, but using another library apollo-cache-persist that automatically persists the in-memory cache to disk (aka, a more permanent medium); reloading it on application load.

When loading application restores the in-memory cache in the App component; src/components/App/index.tsx:
Now that the application, once loaded online the first time, always has the allTodos query cached. Thinking about Apollo Client’s default query behavior, queries are returned from the in-memory cache first, how would the application ever display todos that it did not create?
The solution is, the application, when online, proactively executes the allTodos query when loading; using the network-only fetchPolicy option in the App component; src/components/App/index.tsx:
Mutations Across Refreshes
What happens when the application reloads after executing mutations when offline? The queued mutations are not stored in the in-memory cache and as such do not exist once the application reloads.
note: Another interesting observation is that the optimistic updates are not persisted to disk; as such, also do not exist once the application reloads.
What to do?
Yes, the answer this time is another Apollo Client Link. This time, however, there is not off-the-shelf library to assist, so the application provides its own solution; src/utils/trackerLink.ts.
This Apollo Client Link is inserted before apollo-link-queue. It stores all relevant details of executed mutations in another in-memory cache (Redux). When the mutation resolves, this link removes the details of that mutation.
Much like apollo-cache-persist, the application uses another library, redux-persist, to persist and restore the Redux in-memory cache to disk.
Finally, when the application loads, it uses the stored mutation execution details to re-instantiate the queued mutations; even including the optimistic updates and update functions.

This is all implemented in the App component; src/components/App/index.tsx:
note: The application also has a non-trivial amount of Redux code, e.g., to store and persist the tracked queries.
Conclusion
Wow, this is a long article. At the same time, it points the way towards solving what is a fairly complex problem; highly functional offline applications.