DEV Community

Cover image for Creating offline web apps with AWS Amplify DataStore
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Creating offline web apps with AWS Amplify DataStore

Written by Kay Plößer✏️

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.

LogRocket Free Trial Banner

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.

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

To initialize a React project and add Amplify services to it, use the following command:

$ create-react-app grocerize
$ cd grocerize
$ amplify init
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.


Plug: LogRocket, a DVR for web apps

 
LogRocket Dashboard Free Trial Banner
 
LogRocket is a frontend logging tool 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.
 
Try it for free.


The post Creating offline web apps with AWS Amplify DataStore appeared first on LogRocket Blog.

Top comments (0)