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.