Offline-first made easy with GraphQL, Amplify DataStore and Vue

In order to follow this post you will need a basic knowledge of GraphQL. You can learn the basics following this tutorial at graphql.org. We will be referring to GraphQL schema, directives, types, queries, mutations, subscriptions and resolvers.

Introduction to Offline-first, PWAs and Amplify DataStore

GraphQL knowledge is beneficial when using Amplify DataStore to understand its inner workings but not necessary to use DataStore APIs.

Amplify DataStore data flow in offline mode
Amplify DataStore data flow in online mode

Setting up a new project with the Vue CLI

Creating a new GraphQL API

amplify add api

It’s important that you don’t miss the following steps to set up conflict detection. If you missed it, you can recover with amplify api update

type Chatty @model {
id: ID!
user: String!
message: String!
createdAt: AWSDateTime
}
amplify push
amplify console api

Amplify DataStore setup

npm install --save @aws-amplify/core @aws-amplify/datastore
amplify codegen models

Important: DO NOT forget to regenerate your models every time you introduce a change in your schema.

<amplify-app>
|_ src
|_ models

Using data models allow us to be always compliant with the GraphQL schema and prevent errors right in the client.

import { DataStore } from "@aws-amplify/datastore";
import { Chatty } from "./models";
await DataStore.save(new Chatty({
user: "amplify-user",
message: "Hi everyone!",
createdAt: new Date().toISOString()
}))
import { DataStore, Predicates } from "@aws-amplify/datastore";
import { Chatty } from "./models";
const messages = await DataStore.query(Chatty, Predicates.ALL);

Find all supported filters at query with Predicates.

Creating the UI with Vue

<template>
<div v-for="message of sorted" :key="message.id">
<div>{{ message.user }} - {{ moment(message.createdAt).format('YYYY-MM-DD HH:mm:ss')}})</div>
<div>{{ message.message }}</div>
</div>
</template>
<script>
import { DataStore, Predicates } from "@aws-amplify/datastore";
import { Chatty } from "./models";
import moment from "moment";
export default {
name: 'app',
data() {
return {
user: {},
messages: [],
}
},
computed: {
sorted() {
return [...this.messages].sort((a, b) => -a.createdAt.localeCompare(b.createdAt));
}
},
created() {
// authentication state managament
onAuthUIStateChange((state, user) => {
// set current user and load data after login
switch (state) {
case AuthState.SignedIn: {
this.user = user;
this.loadMessages();
break;
}
}
});
},
methods: {
moment: () => moment(),
loadMessages() {
DataStore.query(Chatty, Predicates.ALL).then(messages => {
this.messages = messages;
});
},
}
}
</script>
<template>
<form v-on:submit.prevent>
<input v-model="form.message" placeholder="Enter your message..." />
<button @click="sendMessage">Send</button>
</form>
</template>
<script>
export default {
data() {
return {
form: {},
};
},
methods: {
sendMessage() {
const { message } = this.form
if (!message) return;
DataStore.save(new Chatty({
user: this.user.username,
message: message,
createdAt: new Date().toISOString()
})).then(() => {
this.form = { message: '' };
this.loadMessages();
}).catch(e => {
console.log('error creating message...', e);
});
},
}
}
</script>
DataStore.delete(Chatty, Predicates.ALL).then(() => {
console.log('messages deleted!');
});

When using Amplify DataStore records are never removed, but marked for removal. Records will then be automatically removed as defined in their TTL attribute. At the time of writing, this is set to 30 days.

As we are using Amplify DataStore, as our source of truth, we don’t need another copy of it at the app level so we just need to load the latest state with loadMessages.

<script>
export default {
data() {
return {
subscription: undefined;
};
},
created() {
//Subscribe to changes
this.subscription = DataStore.observe(Chatty).subscribe(msg => {
console.log(msg.model, msg.opType, msg.element);
this.loadMessages();
});
},
destroyed() {
if (!this.subscription) return;
this.subscription.unsubscribe();
},
}
</script>
Real-time synchronisation using Chrome and Firefox clients side-by-side.

Making Chatty a PWA

vue add @vue/pwa
Service worker serving assets from the cache while offline

Learn more about service workers at using service workers.

yarn build
└── dist
├── css
│ └── app.<version>.css
├── img/icons
│ ├── android-chrome-<size>.png
│ └── ...
├── js
│ ├── app.<version>.png
│ └── ...
├── favicon.ico
├── index.html
├── manifest.json
├── precache-manifest.<version>.json
├── robots.txt
└── service-worker.js
cd dist
python -m SimpleHTTPServer 8887 // open localhost:8887

Remember that in order to test the app you need to install the service worker first. This requires at least loading the app once.

Chatty app while offline.

Adding a PWA custom configuration

// vue.config.js
const manifest = require('./public/manifest.json')
module.exports = {
pwa: {
name: manifest.short_name,
themeColor: manifest.theme_color,
msTileColor: manifest.background_color,
appleMobileWebAppCapable: 'yes',
appleMobileWebAppStatusBarStyle: 'black',
workboxPluginMode: 'InjectManifest',
workboxOptions: {
swSrc: 'src/service-worker.js',
}
}
}
// src/service-worker.js
workbox.core.setCacheNameDetails({ prefix: 'amplify-datastore' })
workbox.core.skipWaiting()
workbox.core.clientsClaim()
const cacheFiles = [{
"revision": "e653ab4d124bf16b5232",
"url": "https://aws-amplify.github.io/img/amplify.svg"
}]
self.__precacheManifest = cacheFiles.concat(self.__precacheManifest || [])
workbox.precaching.precacheAndRoute(self.__precacheManifest, {})

Improving UX while offline

// <div v-if="offline">You are offline.</div>
// <div v-bind:class="{ offline: offline }">
// App.vue
import { Hub } from 'aws-amplify';
export default {
data() {
return { offline: undefined };
},
created() {
this.listener = Hub.listen('datastore', {payload:{event}} => {
if (event === 'networkStatus') {
this.offline = !data.active;
}
})
}
}
Chatty PWA informing the user of changes in the network status.

Publishing your app via the AWS Amplify Console

git init
git remote add origin repo@repoofyourchoice.com:username/project-name.git
git add .git commit -m 'initial commit'git push origin master
AWS Amplify Console deployment steps.

Installing the Chatty app in the Desktop and Mobile

Use add to Home screen to install in Desktop and Mobile.

Cleaning up cloud services

amplify delete

Conclusion

Thanks for reading!

My Name is Gerard Sans. I am a Developer Advocate at AWS Mobile working with AWS Amplify and AWS AppSync teams.

GraphQL is an open source data query and manipulation language for APIs, and a runtime for fulfilling queries with existing data.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store