DEV Community

Samuel
Samuel

Posted on

How to get AWS AppSync running with Offline Support and React Hooks

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 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. 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 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!

What is wora?

As stated already - wora is a collection of libraries. They are created and maintained by @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: 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 to support version 3.0.0 once it is published.

  • @wora/apollo-cache: A library allowing to persist the cache of apollo, which itself is managed by @wora/cache-persist. It also implements 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 by @dabit3 and the apollo documentation 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:

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.

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.

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, 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 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:

Let's also add that to our Apollo exports:

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

Add the following packages:

yarn add graphql-tag @apollo/react-hooks uuid@3.3.3

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:

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!

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:

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.

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!

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!

Top comments (7)

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
jairenee profile image
Jaime Wissner

Additionally, "headers" is not valid to apply to the ApolloClient class from '@wora/apollo-offline'. I had to add the headers to the "createHttpLink" function

Collapse
 
mikerchambers610 profile image
mikeRChambers610

Can you please provide your updated code? Would like to try what you did for my error below:

TypeError: Cannot set property 'isOnline' of undefined

Collapse
 
starpebble profile image
starpebble

I can't decide which is better - amplify or apollo. I simply don't want to throw away any code. Possibly you understand. Amplify js API is different than React-Apollo js useQuery. I like react. I really like GraphQL. Possibly we will all be using react hooks because hooks are inevitable. I wonder how this can be discussed civilly.

Thanks for pointing out solutions to the problems.

Collapse
 
willsamu profile image
Samuel

Amplify is a great tool. It's super quick and easy and the team at AWS is working a lot on it. Yet, for now, it still lacks some key features to possibly replace Apollo. I think in a year maybe it will become the first choice to quickly develop apps with AWS and React - especially if they would develop a more reactish version of it.

Collapse
 
mikerchambers610 profile image
mikeRChambers610

Doesn't seem like this fix works for me since Im not using useQuery or useMutation. I'm using compose to capture all my graphql querys in one export statement. Is there any way to get it to work with the graphql package from react-apollo below?

import {compose, graphql } from 'react-apollo';

Collapse
 
mikerchambers610 profile image
mikeRChambers610

Getting the error :

TypeError: Cannot set property 'isOnline' of undefined