In today’s world, it seems like everyone is always connected. You carry around your smartphone with you at home, at work, on the train, and things are good.
Well, at least until they aren’t.
Even where I live (in Stuttgart, Germany — a city of 600,000 people) there are places where I don’t have any mobile internet connection.
At one subway station in the center of the city, my connection always drops. If I have to wait for the next train, I’m left with what’s on my smartphone and can’t do much else.
This doesn’t have to be the case.
With the DataStore library — the newest addition to the Amplify serverless framework for frontend developers — you can now add offline capabilities to your mobile apps with a few lines of code.
In this article, I’m going to show you how to add AWS Amplify to a React project and enable offline capabilities and synchronization with a cloud backend on AWS.
These points will be illustrated with a simple grocery list application.
The Amplify CLI uses the AWS CLI in the background, so you need to have it set up correctly before you can begin.
You’ll also need Node.js version 12 and NPM version 6.
I used the Cloud9 IDE because it came pre-installed with the AWS CLI, Node.js, and NPM.
First, you need to install two CLIs: create-react-app
and amplify
.
$ npm i -g create-react-app @aws-amplify/cli
We use the create-react-app CLI here because it allows you to set up React apps with a Service Worker very easily. Service Workers are required to show the app in the browser when no internet connection is available.
Getting a page rendered in your browser when you’re offline is only half of the work. The updates you do to the data when using your app needs to be saved somewhere and later synced back to your backend.
That’s where the Amplify framework comes into play. It lets you create AWS powered serverless backends with the help of its CLI, and simplifies the connection of your frontend to these backend services with a JavaScript library.
If you are using Cloud9 and want to use its local AWS profile, you have to add a symlink to the AWS profile. Cloud9 creates and manages a credentials
file, but Amplify searches for a config file.
$ ln -s ~/.aws/credentials ~/.aws/config
To initialize a React project and add Amplify services to it, use the following command:
$ create-react-app grocerize $ cd grocerize $ amplify init
Amplify’s init command will ask you a few questions. You can answer all of these with the defaults — only the environment command requires some input. You can use “dev” here.
It will create deployment infrastructure in the cloud for you.
After this, you should have a React project directory called grocerize. In it should be an Amplify directory with some files.
You need two backend services: auth
and api
.
These two will allow your users to log in and then handle all the synchronization work that needs to be done, so your user’s data doesn’t stay on their devices.
You can add authentication by adding the auth
category to your Amplify project. This is done with the following command:
$ amplify add auth
Again, you can use the defaults for this command. It will create infrastructure as code in the form of CloudFormation templates.
These will later be used to create a serverless authentication service with Amazon Cognito.
GraphQL is the heart of the mechanism used to keep your user’s local data in sync with the cloud.
You need to add a sync-enabled GraphQL API with the Amplify CLI using this command:
$ amplify add api
Here you have to choose “GraphQL”, give the API a speaking name — it’s best to use your React project name “grocerize” — and use “Amazon Cognito User Pool” as the default authorization type so the API can make use of the auth category we added one step earlier.
The next question here is important:
“Do you want to configure advanced settings for the GraphQL API?”
Yes, you want to do that! If you don’t, you will end up with a plain GraphQL API that isn’t sync-enabled, and then the DataStore library will only work offline later.
After you answered that question with “Yes”, you will be asked a few additional questions.
Here are the answers you need to end up with a “sync-enabled GraphQL API”:
? Configure additional auth types? No ? Configure conflict detection? Yes ? Select the default resolution strategy Auto Merge ? Do you have an annotated GraphQL schema? No ? Do you want a guided schema creation? Yes ? What best describes your project: Single object with fields... ? Do you want to edit the schema now? No
Conflict detection is the important part here. If you want more details about that, you can read them in the Amplify docs.
After we added the api
category, the Amplify CLI created some CloudFormation templates we can use to get a AWS AppSync, Lambda, and DynamoDB powered backend initialized in the cloud.
The next step is to generate the models for the DataStore library.
DataStore is implemented in different programming languages for different platforms like Web, Android, and iOS.
It uses GraphQL as the source of truth when generating the model code for the language you use.
First, you need to update the GraphQL schema in:
grocerize/amplify/backend/api/grozerize/schema.graphql
Just replace the content of that file with the following code:
type Item @model @auth(rules: [{allow: owner}]) { id: ID! name: String! amount: Int! purchased: Boolean }
This schema will create a single type that is backed by a DynamoDB table in the cloud and protected by Cognitos authorization mechanisms. Only its creator can read or write it.
The type is an item on our grocery list. It has a name, an amount, and a purchased status.
To get the models as JavaScript classes for your React project, you need to run Amplify’s code generator like this:
$ amplify codegen models
After this, a new directory, grocerize/src/models
, should appear. It holds JavaScript files that can be used by Amplify’s DataStore library later.
Until now, the only thing you deployed was the basic infrastructure Amplify needs to do its work, which is to deploy the actual services that power your application.
To deploy them, just use this command:
$ amplify push
If you get asked to generate GraphQL query code, you can decline. We won’t use GraphQL directly but via the DataStore, and we already generated models for it in the last step.
Only 4 CLI commands and we have a fully-fledged AWS powered serverless backend. Isn’t it crazy what today’s technology enables frontend developers to do?
The command will take a few minutes to complete. Then you have a new JavaScript file, grocerize/src/aws-exports.js
, with all AWS credentials you can use later to configure your frontend.
To connect to the frontend, you need to install the Amplify JavaScript libraries and generate models from your GraphQL code.
To connect your React frontend to your freshly-deployed serverless backend, you need Amplify JavaScript libraries.
You can install them with npm:
$ npm i @aws-amplify/core \ @aws-amplify/auth \ @aws-amplify/ui-react \ @aws-amplify/datastore
The core package contains mostly fundamental Amplify code that is used to connect to the cloud services.
The ui-react
package contains React components for signup and login.
It builds on the auth
package, which does the actual authentication work.
The datastore
package is used to store data locally and sync it via GraphQL to the cloud.
Now, you have to implement the actual frontend code. The first part is the grocerize/src/index.js
.
Replace its content with the following code:
import React from 'react'; import ReactDOM from 'react-dom'; import Amplify from "@aws-amplify/core"; import "@aws-amplify/auth"; import awsconfig from "./aws-exports"; import App from './App'; import * as serviceWorker from './serviceWorker'; Amplify.configure(awsconfig); ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); serviceWorker.register();
So, let’s look at what’s happening here.
Most of this is still a basic React setup. You configured the Amplify library with the aws-exports.js
file that was generated by the Amplify CLI when you deployed our infrastructure with the amplify push
command earlier.
The last call to serviceWorker.register()
at the bottom will enable our React project to get rendered even when the server isn’t reachable anymore, thus making it available offline.
The next file to update is grocerize/src/App.js
. Replace its content with the following code:
import React from "react"; import { DataStore, Predicates } from "@aws-amplify/datastore"; import { withAuthenticator } from "@aws-amplify/ui-react"; import { Item } from "./models"; class App extends React.Component { state = { itemName: "", itemAmount: 0, items: [], }; async componentDidMount() { await this.loadItems(); DataStore.observe(Item).subscribe(this.loadItems); } loadItems = async () => { const items = await DataStore.query(Item, Predicates.ALL); this.setState({ items }); }; addItem = async () => { await DataStore.save( new Item({ name: this.state.itemName, amount: this.state.itemAmount, purchased: false, }) ); this.setState({itemAmount: 0, itemName: ""}); } purchaseItem = (item) => () => DataStore.save( Item.copyOf(item, (updated) => { updated.purchased = !updated.purchased; }) ); removeItem = (item) => () => DataStore.delete(item); render() { const { itemAmount, itemName, items } = this.state; return ( <div style={{ maxWidth: 500, margin: "auto" }}> <h1>GROCERIZE</h1> <h2>Your Personal Grocery List</h2> <p>Add items to your grocery list!</p> <input placeholder="Gorcery Item" value={itemName} onChange={(e) => this.setState({ itemName: e.target.value })} /> <input placeholder="Amount" type="number" value={itemAmount} onChange={(e) => this.setState({ itemAmount: parseInt(e.target.value) }) } /> <button onClick={this.addItem}>Add</button> <h2>Groceries:</h2> <ul style={{listStyleType:"none"}}> {items.map((item) => ( <li key={item.id} style={{fontWeight: item.purchased? 100 : 700}}> <button onClick={this.removeItem(item)}>remove</button> <input type="checkbox" checked={item.purchased} onChange={this.purchaseItem(item)}/> {" " + item.amount}x {item.name} </li> ))} </ul> </div> ); } } export default withAuthenticator(App);
This code is the only screen that our app uses.
At the top, we import the DataStore library, the Item model that was generated from your GraphQL schema, and a higher order component for authentication.
The DataStore is used by the App component to load all items when it renders.
After the items got rendered for the first time, the component subscribes to changes to any item in the DataStore with its loadItems
method.
This method will gather all items and cause a re-render of the screen.
The loadItems
, addItem
, purchaseItem
and removeItem
methods form the basic CRUD operations of this app. The programming model is very simple because you store your data locally, which happens almost instantly, and in the background everything gets synchronized with your cloud infrastructure.
Before the App component is exported, it gets wrapped into the withAuthenticator
higher order component that will show a login screen before a user can interact with the app.
To start the development server, just run this command:
$ npm start
If you open the development server URL in the browser, you will be prompted to sign up.
As you can see in the signup and login process, the backend comes preconfigured with secure password requirements and email activation. This can all be configured.
After the login, you can create your grocery list as you like.
If you use this application on your smartphone and your internet connection drops inside the grocery store, you can still interact with your grocery list, check what you have bought, etc.
When the connection comes back on later, all your changes will be synced to the backend, so you can modify them on another device.
AWS Amplify is a very powerful tool for frontend developers to have in their back pocket, and the new DataStore with its offline and synchronization capabilities make it even more pleasant to work with.
The programming model is kept simple and all the heavy lifting is done by DataStore in the background.
In a few minutes, you can create a serverless backend with everything the average mobile app needs and the fact that Amplify uses CloudFormation to deploy AWS services makes it versatile and future proof.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
15 Replies to "Creating offline web apps with AWS Amplify DataStore"
Thanks for the post, I’m new to amplify and tried to follow along with my own api. Only problem is now my queries and mutations have the following added, any pointers would be great, thank you!!
_version
_deleted
_lastChangedAt
Hey Al,
Those fieldas are required so DataStore & AppSync can synchronize.
Otherwise AppSync wouldn’t know what changed since you went offline.
Thanks, the penny is starting to drop slowly but surely! I’m in mock mode, can graphiql be used to make queries or mutations synced with Datastore?
In the AWS Console under
AppSync -> APIs -> -> Queries
You’ll find a graphiql client for you API.
Thanks for the post, very well written. Is there anyway to make the local storage only contain your own Items and not everyone’s?
I think the default owner authorization only allows the creator of an item to read it.
From what I can see, that’s true for the app, but in my PoC, if I inspect the local DB attached to the application you can see data from all owners in plaintext. This is a showstopper for us, so I’m hoping it’s just that I’ve done something wrong configuring it.
Hmmm, well I’ve just run through your example above verbatim and I only see the items I should in the local Indexed DB, so maybe I spoke too soon. I wonder if the problem is trying to retrofit the datastore into an existing application.
So, thanks for a well written example and thanks for prompting me to have another look at datastore!
No problem.
You can also reach out to the Amplify team on Twitter, they are usually very responsive!
Actually, that’s not the case. The local writes weren’t syncing to the backend, which is why they only appeared to be in the correct local indexed db, so it is a problem. I’ll raise a ticket with the datastore project, rather than polluting your comments. Thanks again
Thanks for the post! I have completed the whole tutorial with the latest stable versions of all the amplify libraries and almost everything works! I have the app running on two different browsers, logged in with the same user. When I update the grocery list on browser one, the list doesn’t automatically update on browser two… It only updates after I reload the page. It should update automatically, right?
I’m not sure how stable DataStore is yet… just started working with it some days ago and the documentation is lacking. Any help would be greatly appreciated. In my scenario above, browser two should update after changes in browser one, right? without having to reload the browser page?
Again, thank you very much for this tutorial!
Santi
Hi,
I hit this issue, the fix is here:
https://github.com/aws-amplify/amplify-js/issues/5687
Summary is, you need to explicitely include the owner in the model.
Hope that helps,
Steve
Hi Santiago,
Was it working as expected at any stage for you?
In my case I had already set up an Amplify project and tried to add Datastore to my project which did not work for me either.
So I started a brand new amplify project following a yt video from Nader Dabit called ‘…Offline first…’
I followed that and it worked for me.
Alan
Hi Alan,
Sync between two browsers never worked for me following this tutorial… Everything else has been working fine though. My general approach to learning and getting Amplify to work has been doing multiple tutorials from different sites until I find the steps that work… Then, I go and recreate all the steps in a new app, making sure I didn’t miss anything and writing my own notes.
I also did some tutorials from Nader Dabit, not sure if the tutorial I did from him included Authentication and DataStore. I will check the one you’re recommending. Thanks for the help!
Santiago
I’ve read that AppSync doesn’t work on mobile, only web. Is that still true?