How to get AWS AppSync running with Offline Support and React Hooks
Published: 2020-05-24 15:44:38

## Motivation

AWS provides some extremely powerful and useful tools for developers. However, their priorities sometimes seem to be more focused on developing new products rather than maintaining the ones currently in use.

This seems to be especially true when it comes to their Amplify and AppSync SDKs - [Amplify Datastore](https://aws.amazon.com/de/blogs/aws/amplify-datastore-simplify-development-of-offline-apps-with-graphql/) looks like a promising tool which has been in development for over two years while their React AppSync SDK did not get the attention it needs:

As of writing this article, the `aws-appsync` package has a dependency on `apollo-client@2.4.6`, which comes with a lot of problems when trying to develop apps with up-to-date technology. This setup won't allow using `@apollo/react-hooks` which only function properly with `react-apollo@2.6`. It will also result in a lot of unwanted side effects when using it with other libraries, e.g. throwing the *componentWillReceiveProps* warning with `react@16.9` or even runtime errors when using other react native libraries which rely on `react-native-community/netinfo` at later versions.

AWS's solution to this problem is [publishing these appsync link packages for usage with react-apollo] (https://github.com/awslabs/aws-mobile-appsync-sdk-js/issues/450#issuecomment-541230344). This works just fine - as long as there is no offline support required, which is almost always essential when developing mobile apps.

And this is where [**wora**](https://github.com/morrys/wora) comes into play: A collection of libraries which, when combined, will do exactly what we want!

**TLDR: A repository containing all code developed for this article can be found [here](https://github.com/willsamu/appsync-offline-mutations)!**

 

## What is wora?

As stated already - wora is a collection of libraries. They are created and maintained by [@morrys](https://github.com/morrys) and go by the beautiful name **write once render anywhere**. With some additional setup, we can configure them to create perfectly functional offline-ready applications together with AWS AppSync.

The libraries we are going to use are:

* [`@wora/apollo-offline`](https://morrys.github.io/wora/docs/apollo-offline): A layer on top of the `apollo-client` which implements offline mutations and its persistence. It depends on the currently latest version of apollo-client (2.6) and there is a [roadmap](https://github.com/morrys/wora/issues/25) to support version 3.0.0 once it is published.

* [`@wora/apollo-cache`](https://morrys.github.io/wora/docs/apollo-cache): A library allowing to persist the cache of `apollo`, which itself is managed by [`@wora/cache-persist`](https://morrys.github.io/wora/docs/cache-persist). It also implements [`apollo-cache-inmemory`](https://www.npmjs.com/package/apollo-cache-inmemory), so that we can take full advantage of its functionalities.


Okay, enough talking - let's get coding!

## Pre-requisites

We will create a simple react native todo-app implementing AWS Cognito User Pool authentication. For the sake of simplicity, this article assumes you know what Amplify and its corresponding tools do and you have worked with them and react-apollo before. If you haven't, I suggest you check out [this repository](https://github.com/dabit3/awesome-aws-amplify) by @dabit3 and the [apollo documentation](https://www.apollographql.com/docs/react/) before proceeding.


## Initial Setup

First, install Amplify and Expo globally.
```
npm install -g @aws-amplify/cli expo-cli
```


Create a react native project with **expo init**. In that directory, set up Amplify and add User Pool authentication with the standard settings.

Next, run **amplify add api**, select **GraphQL** with **Amazon Cognito User Pool** as authorization type. For this article, we will use the guided Todo schema creation and also select automated code generation. 

Finally, run **amplify push** to create the resources on AWS.

We will then add some libraries to get our project running with authentication:

```
yarn add @react-native-community/netinfo aws-amplify aws-amplify-react-native
```

Now update the **App.js**:

{% gist https://gist.github.com/willsamu/99e7a6b27bd08df029250ec4d3907afc/95d38aeadd89054840beaa87cf946d13b653a910 file=App.js %}

Once we've done that, we can run the app and create a user, which we will use to authenticate our requests.

## Apollo-Offline Client Setup

Now it's time to add the offline client to our app.

```
yarn add @wora/apollo-offline @wora/apollo-cache apollo-client react-apollo @react-native-community/async-storage
```

We will also need to add the link packages mentioned earlier:

```
yarn add apollo-link apollo-link-http aws-appsync-auth-link aws-appsync-subscription-link 
```

To set it up, add a folder **src/Apollo** with two files, **index.js** and **client.js**. 

{% gist https://gist.github.com/willsamu/99e7a6b27bd08df029250ec4d3907afc/95d38aeadd89054840beaa87cf946d13b653a910 file=src_apollo_index.js %}

{% gist https://gist.github.com/willsamu/99e7a6b27bd08df029250ec4d3907afc/8a5d973520b69335a2212aba1c659ea1b331cc95 file=src_apollo_client.js %}

Although there are quite a few imports in the **client.js** file, the whole setup is very similar to any other initialization of the apollo client. To break it down real quick: 

First, we grab all the necessary data we need to communicate with our backend from our **aws-exports.js** file and forge it into a single object for the apollo client. To do so, we use the packages provided by AWS and *ApolloLink*. It is important to note that we pass the JWT token to the auth link, and also, as authorization option, to the header of the client. If you want to use another method, such as IAM or API key, you need to keep that in mind. 

As the second argument, we can optionally pass *persistOfflineOptions* to the client. They work in the same way as they do for the *AppSyncClient* and can be used to enable multi-user support.

When we create the cache, we can pass any options suitable for *InMemoryCache*, for example setting up a custom *dataIdFromObject* function, *cacheRedirects* or  other attributes to [optimize Apollo's performance](https://github.com/apollographql/apollo-client/pull/4543).

Last but not least, we set the offline options of the client. They are mostly specified through functions, which will be invoked when there is a mutation ready to be dispatched to the backend. More information can be found in the [docs](https://morrys.github.io/wora/docs/apollo-offline.html#apollo-client-with-offline-options), but we'll get back to that later. 

Before we pass the client with the *ApolloProvider* context to our app, there is one more thing to be done.

## Rehydration

The `apollo-offline` package offers a [*useQuery*](https://morrys.github.io/wora/docs/apollo-offline.html#usequery) hook, which, if the client is not hydrated, invokes the rehydration request and rerenders once it is resolved. But depending on the application, it might be better to do that during app startup. That's why we'll create our own rehydration component: 

{% gist https://gist.github.com/willsamu/99e7a6b27bd08df029250ec4d3907afc/c7ef9bf88853d16a1c0f4e72f6839934888a7c33 file=src_apollo_rehydration.js %}

Let's also add that to our Apollo exports:

{% gist https://gist.github.com/willsamu/99e7a6b27bd08df029250ec4d3907afc/839211cdca8142a8ad9dbf2db7238714e1baa109 file=src_apollo_index.js %}

Now that we are done setting everything up, we can update our **App.js**. I will also add a component to test the app.

{% gist https://gist.github.com/willsamu/99e7a6b27bd08df029250ec4d3907afc/e6f8a525b95fd4a5fdb938d1c417224b37cd42f5 file=App.js %}

Add the following packages:
```
yarn add graphql-tag @apollo/react-hooks uuid@3.3.3
```

{% gist https://gist.github.com/willsamu/99e7a6b27bd08df029250ec4d3907afc/b287dfff4214b44329f00363808593c854b4e7dc file=src_TestComponent_index.js %}

{% gist https://gist.github.com/willsamu/99e7a6b27bd08df029250ec4d3907afc/1b16825677620aa6b571c3b09e056e9e2310d077 file=src_TestComponent_TestComponent.js %}

First, we create two graphql document nodes that will be used by the useQuery and useMutation hooks.

The *handleUpdateTodo* function will be invoked once the client receives a successful response for the *createTodo* mutation from AppSync. The apollo cache will be updated, and since useQuery returns a watchable query, it will respond to the result of the mutation. 

The UI will simply render an input field with a button to create a todo-task and the name of the created tasks will be mapped as a text field.

Basically, we are all done here. If the device is offline, any mutation created will be saved and pushed to AppSync once the connection is established again. But as mentioned, we will get back to the offline options of the client!

## One More Thing

As you have probably tested already, our app performs exactly as we would expect it with the AppSyncClient. We can use beautiful hooks, have persistent offline storage - and all of that without any hacky tricks. But there is one last thing to do. 

You have probably observed that adding todos while the app is online results in an almost instant UI update displaying the todo item. However, the app performs differently when the device is on airplane mode:

Once the mutation has been invoked and the device is online again, it will be sent to AppSync and the result can be observed in the DynamoDB table. Even though this is expected behavior, the useQuery will not update. 

There are two ways to handle this issue. Both should probably be used together in production environments. 

### OnMutationCompleteHandler

In order to display our actual backend in the app, we can make use of the `onComplete` part of the offline options. 

`onComplete` will be called once the request has successfully been completed and invokes a callback, which needs to return true in order to remove the request from the offline mutation queue. When invoking the callback, it also passes an *options* object to it. That object contains information about the successful mutation - and amongst other things the exact same object which is returned by the casual *update* function from useMutation. With that knowledge, we can implement an `onMutationCompleteHandler`!

First, export your `handleUpdateTodo` function from **TestComponent.js** if you haven't done that already. Then import that in the handler we will create:

{% gist https://gist.github.com/willsamu/99e7a6b27bd08df029250ec4d3907afc/398f006e777c94760ea8b9a9dd75d2991f2189a3 file=src_apollo_onMutationCompleteHandler.js %}

In this handler, we can implement any update function for any potential mutation our app will process. Pretty nice, huh?

As the last thing left to do, we will update the client to actually pass the data and invoke it in the onComplete call!

{% gist https://gist.github.com/willsamu/99e7a6b27bd08df029250ec4d3907afc/1a9f3080a47a35292852f0f9532e8444ebc1aca4 file=src_apollo_clientUpdates.js %}

### Optimistic Responses

While the handler we just implemented always displays the actual state of our database, we have to keep in mind that the app can potentially stay offline for a long period. As of now, there would be no changes in the UI until the app is back online, which results in pretty bad user experience. 

Although we can just pass a casual optimistic response object to a mutation, there are a few things to keep in mind when developing offline apps:

An optimistic response is designed to display immediate feedback to user interaction and is usually resolved within seconds. This is not the case for an offline app; it might take hours or even days until the connection is established again. During that time, the app could be closed by the user or the operating system. Since optimistic responses are handled by many internal API calls in the `apollo-client`, it is hard to persist them in the cache. 

In order to avoid data inconsistencies, the `apollo-offline` client handles optimistic responses provided to an offline mutation as the actual response to the mutation. The optimistic response will be inserted in the cache; immediately after that, the update callback will be invoked with the same input parameters as the optimistic response. Once the cache-entry is not optimistically inserted anymore, it can be easily persisted.

This approach will result in a much better user experience but should be used with caution: The cache won't be informed in the case of a mutation returning a result unequal to the optimistic one. To ensure the reliability of your cached data, you should still implement a handler to the *onComplete* call. Just to give you an idea, your code would look something like this:

{% gist https://gist.github.com/willsamu/99e7a6b27bd08df029250ec4d3907afc/798b640f26280aa583991959b28232445a61c2c7 file=optimisticErrorHandling.js %}

Obviously, you will need to put in some additional effort to handle all error cases. If you are not familiar with apollo error handling in general, I suggest you check out [this blog post](https://www.apollographql.com/blog/full-stack-error-handling-with-graphql-apollo-5c12da407210).

Additionally, in some rare cases you might need the *onDiscard* callback of the offline options. It is invoked in some specific error situations, I suggest you just try logging it while testing. From there you can decide how to proceed with the failed mutation. 

One last note: Some applications actually rely on optimistic responses. For example, if you want to display visual feedback in a chat app on whether a message has been delivered to the server, you would check if that message was inserted optimistically or not. In order to handle such scenarios with `apollo-offline`, you can just add an additional flag to the data written to the store, which you then modify in the *onComplete* call.

## That's it

Congratulations, you are ready to develop offline-ready apps using AWS! I hope this article will help some other developers who are also struggling with the current appsync packages. You can find the working app example in [this GitHub repository](https://github.com/willsamu/appsync-offline-mutations)!

Special thanks to @morrys at this point for creating wora and also providing some clarification on some technical aspects.

If you feel I've missed something or could improve this article, please don't hesitate to let me know in the comments!