Offline-First Data with AWS Amplify
Published: 2022-06-27 12:44:14


[Amplify DataStore](https://docs.amplify.aws/lib/datastore/getting-started/q/platform/js/) is an AWS Amplify feature that provides a persistent on-device storage bucket to read, write, and observe data online or offline without additional code. 

With DataStore, we can persist data locally on our device, making working with shared cross-user data just as simple as working with local-only data.

This post will discuss setting up a DataStore environment by building a simple blog application with React.js.

The complete source code of this project is on this [Github Repository](https://github.com/Djcodebeast/aws-datasore-manipulations). 

## Prerequisites 

The following are requirements in this post:


- Basic knowledge of JavaScript and React.js.
- Node.js and AWS CLI are installed on our computers.
- AWS Amplify account; create one [here](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html?nc1=f_ct&src=default).


## Getting Started

We'll run the following command in our terminal:

```sh
npx create-react-app datastore-blog
```

The above command creates a react starter application in a folder; **datastore-blog.**

Next, we'll navigate into the project directory and bootstrap a new amplify project with the following commands:

```sh
cd datastore-blog # to navigate into the project directory
npx amplify-app # to initialize a new amplify project
```

Next, we'll install amplify/core, amplify/datastore, and react-icons libraries with the following command:

```sh
npm install @aws-amplify/core @aws-amplify/datastore react-icons
```

## Building the application

First, let's go inside the amplify folder and update the `schema.graphql` file with the following code:

```js
    //amplify/backend/api/schema.graphql
    type Post @model {
      id: ID!
      title: String!
      body: String
      status: String
    }
```
In the code above, we instantiated a `Post` model with some properties.

AWS amplify datastore uses data model(s) defined in the `schema.graphql` to interact with the datastore API.

Next, let's run the following command:

```sh
npm run amplify-modelgen
```

The command above will inspect the `schema.graphql` file and generate the model for us.

We should see a **model** folder with the generated data model inside the `src` directory.

Next, we'll run `amplify init` command and follow the prompts to register the application in the cloud:



![](https://paper-attachments.dropbox.com/s_4C6F9C3EE9B82EDB286D106E99BC2233D165494A78CC59CAD2C068AD0D6A501A_1651367527384_datastore-init.png)


Next, we'll deploy our applications to the cloud with the following command:

```sh
amplify push
```

After deploying the application, we'll head straight to `index.js` and configure AWS for the application's UI.

```js
//src/index.js
    
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
    
import "bootstrap/dist/css/bootstrap.min.css";
import Amplify from "@aws-amplify/core";
import config from "./aws-exports";
    
Amplify.configure(config);
    
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <App />
      </React.StrictMode>
);
```

In the code snippets above, we:


1. Imported bootstrap minified CSS for styling the application.
2. We imported `Amplify` and `Config` and did the Amplify configuration.

Now, let's head over to `App.js` and update it with the following snippets:

```js
//src/App.js

import React, { useState, useEffect } from "react";
import { DataStore } from "@aws-amplify/datastore";
import { Post } from "./models";
import { Form, Button, Card } from "react-bootstrap";
import { IoCreateOutline, IoTrashOutline } from "react-icons/io5";
import "./App.css";
    
const initialState = { title: "", body: "" };
    
const App = () => {
const [formData, setFormData] = useState(initialState);
const [posts, setPost] = useState([]);
    
useEffect(() => {
  getPost();
  const subs = DataStore.observe(Post).subscribe(() => getPost());
  return () => subs.unsubscribe();
});
    
const handleChange = (e) => {
  setFormData({ ...formData, [e.target.name]: e.target.value });
};
    
async function getPost() {
  const post = await DataStore.query(Post);
  setPost(post);
}
    
async function createPost(e) {
  e.preventDefault();
  if (!formData.title) return;
    await DataStore.save(new Post({ ...formData }));
    setFormData(initialState);
}
    
async function deletePost(id) {
const auth = window.prompt(
    "Are sure you want to delete this post? : Type yes to proceed"
  );
if (auth !== "yes") return;
const post = await DataStore.query(Post, `${id}`);
DataStore.delete(post);
}
    
return (
  <>
    <div className="container-md">
      <Form className="mt-5" onSubmit={(e) => createPost(e)}>
        <h1 className="text-center">AWS Datastore Offline-First Data Manipulations</h1>
        <Form.Group className="mt-3" controlId="formBasicEmail">
          <Form.Control
            type="text"
            placeholder="Enter title"
            value={formData.title}
            className="fs-2"
            onChange={handleChange}
            name="title"
          />
        </Form.Group>
        <Form.Group className="mb-3" controlId="exampleForm.ControlTextarea1">
          <Form.Control
            size="lg"
            as="textarea"
            rows={3}
            required
            className="fs-5"
            name="body"
            onChange={handleChange}
            value={formData.body}
            placeholder="Write post"
          />
        </Form.Group>
        <div className="d-md-flex justify-content-md-end">
          <Button variant="primary" type="submit">
             Create Post
          </Button>
        </div>
      </Form>
      <div>
        {posts.map((post) => (
          <Card key={post.id} className="mt-5 mb-5 p-2">
            <Card.Body>
              <Card.Title className="fs-1 text-center">
                {post.title}
              </Card.Title>
                <Card.Text className="fs-4 text-justify">{post.body}</Card.Text>
            </Card.Body>
            <div className="d-md-flex justify-content-md-end fs-2 p-3 ">
              <h1>
                <IoCreateOutline style={{ cursor: "pointer" }} />
              </h1>
              <h1 onClick={() => deletePost(post.id)}>
                <IoTrashOutline style={{ cursor: "pointer" }} />
              </h1>
            </div>
          </Card>
        ))}
      </div>
    </div>
  </>
 );
 };
export default App;
```
In the code snippets above, we did the following:


1. Imported `Datastore` from "@aws-amplify/datastore" and the Post model from "../models/Post".
2. Imported `Card`, `Form`, and `Button` from "react-bootstrap" and `IoCreateOutline` and `IoTrashOutline` from "react-icons/io5"
3. Initialized constant properties `title` and `body` and created `formData` constant with useState hook and passed the initial state to it.
4. Created `posts` state constant to hold all our posts when we fetch them from the database. 
5. Used the `handleChange` function to handle changes in our inputs
6. Used the `getPost` function to get all the posts from the database and update the `posts` state
7. Used the `createPost` function to save our inputs and the `deletePost` function to delete a particular post.
8. Used `Form` and `Button` to implement our form inputs, then looped through posts and used `Card` and the icons from "react-icons/io5" to display the posts if we have some.

In the browser, we'll have the application like the below:


![](https://paper-attachments.dropbox.com/s_4C6F9C3EE9B82EDB286D106E99BC2233D165494A78CC59CAD2C068AD0D6A501A_1651627750574_datastore.gif)


The edit button does nothing now; let's create a component that will receive the post id and give us a form to update the post title and body.

Next, let's create a Components folder inside the `src` folder and create a `UpdatePost.js` file with the following snippets:

```js
//Components/UpdatePost.js
import React, { useState } from "react";
import { Form, Button } from "react-bootstrap";
import { Post } from "../models";
import { DataStore } from "@aws-amplify/datastore";
import { IoCloseOutline } from "react-icons/io5";
    
const editPostState = { title: "", body: "" };
    
function UpdatePost({ post: { id }, setShowEditModel }) {
  const [updatePost, setUpdatePost] = useState(editPostState);
      
  const handleChange = (e) => {
    setUpdatePost({ ...updatePost, [e.target.name]: e.target.value });
  };
    
  async function editPost(e, id) {
    e.preventDefault();
    const original = await DataStore.query(Post, `${id}`);
    if (!updatePost.title && !updatePost.body) return;
      await DataStore.save(
        Post.copyOf(original, (updated) => {
        updated.title = `${updatePost.title}`;
        updated.body = `${updatePost.body}`;
        })
      );
    setUpdatePost(editPostState);
    setShowEditModel(false);
  }
    
return (
  <div className="container">
    <Form
      className="mt-5 border border-secondary p-3"
      onSubmit={(e) => editPost(e, id)}
    >
      <h1
        className="d-md-flex justify-content-md-end"
        onClick={() => setShowEditModel(false)}
      >
        <IoCloseOutline />
      </h1>
      <Form.Group className="mt-3" controlId="formBasicEmail">
        <Form.Control
          type="text"
          placeholder="Enter title"
          className="fs-3"
          value={updatePost.title}
          onChange={handleChange}
          name="title"
        />
      </Form.Group>
      <Form.Group className="mb-3 " controlId="exampleForm.ControlTextarea1">
        <Form.Control
          size="lg"
          as="textarea"
          required
          name="body"
          className="fs-4"
          onChange={handleChange}
          value={updatePost.body}
          placeholder="Write post"
        />
      </Form.Group>
      <div>
        <Button variant="primary" type="submit">
          Update Post
        </Button>
      </div>
    </Form>
  </div>
 );
}
export default UpdatePost;
```

In the code above, we:


- Initialised `editPostState` object, created `updatePost` with react’s useState hook and passed `editPostState` to it.
- Destructured `id` from the post property that we would get from the `App.js` .
- Created `handleChange` function to handle changes in the inputs.
- Created `editPost` function to target post with the `id` and updated it with the new input values.
- Used `Form` and `Button` from "react-bootstrap" and implemented the inputs form.
- Used `IoCloseOutline` from "react-icons" to close the inputs form.

Next, let's import the `UpdatePost.js` file and render it inside `App.js` like below:

```js
//src/App.js
import React, { useState, useEffect } from "react";
//other imports here
import UpdatePost from "./Components/UpdatePost";
    
const initialState = { title: "", body: "" };
    
const App = () => {
  const [formData, setFormData] = useState(initialState);
  const [posts, setPost] = useState([]);
  const [postToEdit, setPostToEdit] = useState({});
  const [showEditModel, setShowEditModel] = useState(false);
      
  //useEffect function here
    
  //handleChange function here
    
  //getPost function here
    
  //createPost function here
     
  //deletePost function here
    
return (
  <>
    <div className="container-md">
      {/* Form Inputs here */}
    
      <div>
        {posts.map((post) => (
          <Card key={post.id} className="mt-5 mb-5 p-2">
            <Card.Body>
              <Card.Title className="fs-1 text-center">
                {post.title}
              </Card.Title>
              <Card.Text className="fs-4 text-justify">{post.body}</Card.Text>
            </Card.Body>
            <div className="d-md-flex justify-content-md-end fs-2 p-3 ">
              <h1
              onClick={() => {
              setPostToEdit(post);
              setShowEditModel(true);
              }}
              >
              <IoCreateOutline style={{ cursor: "pointer" }} />
              </h1>
              <h1 onClick={() => deletePost(post.id)}>
                <IoTrashOutline style={{ cursor: "pointer" }} />
              </h1>
            </div>
          </Card>
        ))}
          {showEditModel && (
            <UpdatePost post={postToEdit} setShowEditModel={setShowEditModel} />
          )}
      </div>
    </div>
  </>
);
};
export default App;
```

In the code above, we :


- Imported `UpdatePost` from the Components folder
- Created `postToEdit` to target the particular post we'll be updating and `showEditModel` to show the input form; with the useState hook.
- Set an onClick function on the edit icon to update the `postToEdit` and `showEditModel` states.
- Conditionally rendered the `UpdatePost` component and passed `postToEdit` and `setShowEditModel` to it.

When we click the edit icon in the browser, we would see a form to fill and update a post.


![](https://paper-attachments.dropbox.com/s_4C6F9C3EE9B82EDB286D106E99BC2233D165494A78CC59CAD2C068AD0D6A501A_1651672423122_editPost.gif)


There is a list of other notable features of the AWS Datastore that we did not cover in this post; see the AWS DataStore [documentation.](https://docs.amplify.aws/lib/datastore/getting-started/q/platform/js/) 


## Conclusion 

 This post discussed setting up the AWS DataStore environment and building a simple blog posts application with React.js.
 

##  Resources

The following resources might be helpful.


- [Introduction to AWS Datastore](https://docs.aws.amazon.com/whitepapers/latest/amplify-datastore-implementation/introduction.html)
- [Getting started with AWS Datastore](https://docs.amplify.aws/lib/datastore/getting-started/q/platform/js/).