Kay Plößer Software Engineer and Web Enthusiast.

Creating offline web apps with AWS Amplify DataStore

7 min read 2089

The AWS logo against a white background.

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.

Prerequisites

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.

We made a custom demo for .
No really. Click here to check it out.

I used the Cloud9 IDE because it came pre-installed with the AWS CLI, Node.js, and NPM.

CLI installation

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.

Project setup

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.

Adding AWS backend services

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.

Adding authentication

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.

Adding a GraphQL API

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.

Generate data models

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.

Deploy the Backend Services

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.

Connect the frontend

To connect to the frontend, you need to install the Amplify JavaScript libraries and generate models from your GraphQL code.

Install the frontend libraries

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.

Update the frontend code

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.

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

Conclusion

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.

You come here a lot! We hope you enjoy the LogRocket blog. Could you fill out a survey about what you want us to write about?

    Which of these topics are you most interested in?
    ReactVueAngularNew frameworks
    Do you spend a lot of time reproducing errors in your apps?
    YesNo
    Which, if any, do you think would help you reproduce errors more effectively?
    A solution to see exactly what a user did to trigger an errorProactive monitoring which automatically surfaces issuesHaving a support team triage issues more efficiently
    Thanks! Interested to hear how LogRocket can improve your bug fixing processes? Leave your email:

    : Full visibility into your web apps

    LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

    In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

    .
    Kay Plößer Software Engineer and Web Enthusiast.

    14 Replies to “Creating offline web apps with AWS Amplify DataStore”

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

    2. Hey Al,

      Those fieldas are required so DataStore & AppSync can synchronize.

      Otherwise AppSync wouldn’t know what changed since you went offline.

    3. 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?

    4. Thanks for the post, very well written. Is there anyway to make the local storage only contain your own Items and not everyone’s?

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

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

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

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

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

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

    Leave a Reply