Offline-First Data with AWS Amplify
Christian Nwamba
Posted on June 27, 2022
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
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
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
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
}
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
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
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>
);
In the code snippets above, we:
- Imported bootstrap minified CSS for styling the application.
- We imported
Amplify
andConfig
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;
In the code snippets above, we did the following:
- Imported
Datastore
from "@aws-amplify/datastore" and the Post model from "../models/Post". - Imported
Card
,Form
, andButton
from "react-bootstrap" andIoCreateOutline
andIoTrashOutline
from "react-icons/io5" - Initialized constant properties
title
andbody
and createdformData
constant with useState hook and passed the initial state to it. - Created
posts
state constant to hold all our posts when we fetch them from the database. - Used the
handleChange
function to handle changes in our inputs - Used the
getPost
function to get all the posts from the database and update theposts
state - Used the
createPost
function to save our inputs and thedeletePost
function to delete a particular post. - Used
Form
andButton
to implement our form inputs, then looped through posts and usedCard
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;
In the code above, we:
- Initialised
editPostState
object, createdupdatePost
with react’s useState hook and passededitPostState
to it. - Destructured
id
from the post property that we would get from theApp.js
. - Created
handleChange
function to handle changes in the inputs. - Created
editPost
function to target post with theid
and updated it with the new input values. - Used
Form
andButton
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;
In the code above, we :
- Imported
UpdatePost
from the Components folder - Created
postToEdit
to target the particular post we'll be updating andshowEditModel
to show the input form; with the useState hook. - Set an onClick function on the edit icon to update the
postToEdit
andshowEditModel
states. - Conditionally rendered the
UpdatePost
component and passedpostToEdit
andsetShowEditModel
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.
Posted on June 27, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.