Offline-First Data with AWS Amplify

Christian Nwamba - Jun 27 '22 - - Dev Community

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

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.

Getting Started

We'll run the following command in our terminal:

npx create-react-app datastore-blog
Enter fullscreen mode Exit fullscreen mode

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:

cd datastore-blog # to navigate into the project directory
npx amplify-app # to initialize a new amplify project
Enter fullscreen mode Exit fullscreen mode

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

npm install @aws-amplify/core @aws-amplify/datastore react-icons
Enter fullscreen mode Exit fullscreen mode

Building the application

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

    //amplify/backend/api/schema.graphql
    type Post @model {
      id: ID!
      title: String!
      body: String
      status: String
    }
Enter fullscreen mode Exit fullscreen mode

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:

npm run amplify-modelgen
Enter fullscreen mode Exit fullscreen mode

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:

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

amplify push
Enter fullscreen mode Exit fullscreen mode

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

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

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:

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

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:

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:

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

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:

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

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.

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.

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .