Serverless React Web App with AWS Amplify — Part Two

James Hamann
Apr 24, 2018 · 15 min read

Following on from my previous post, Serverless React Web App with AWS Amplify — Part One, today we’ll layout the Front End and implement our basic CRUD Functions.

All source code for the project can be found here.

Front End

To bootstrap the front end, we’ll use React Semantic UI, a simple UI Framework that provides a great set of React Components.

#bash$ yarn add semantic-ui-react
[...]
$ yarn add semantic-ui-css

To get the CSS styling, you’ll need to import it in your index.js file, like so.

# index.jsimport React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import 'semantic-ui-css/semantic.css'
import registerServiceWorker from './registerServiceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();

Now let’s setup the folder structure. I like to keep screens in one folder and re-useable components in another. With that it mind, let’s make two new directories within the src directory.

#bash$ mkdir src/components
$ mkdir src/screens
# Project Layout├── README.md
├── awsmobilejs
│ ├── #current-backend-info
│ │ ├── aws-exports.js
│ │ ├── backend-details.json
│ │ ├── cloud-api
│ │ │ └── sampleLambda
│ │ │ ├── app.js
│ │ │ ├── lambda.js
│ │ │ ├── package-lock.json
│ │ │ └── package.json
│ │ └── mobile-hub-project.yml
│ └── backend
│ ├── cloud-api
│ │ └── sampleLambda
│ │ ├── app.js
│ │ ├── lambda.js
│ │ ├── package-lock.json
│ │ └── package.json
│ └── mobile-hub-project.yml
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── aws-exports.js
│ ├── components
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ ├── registerServiceWorker.js
│ └── screens
└── yarn.lock

We’ll obviously need a few different views here, one to display all our items, one to create our items, one to edit/delete our items and one which shows a little more information about the item. We’ll use our App.js as the dashboard/index view for our app and add a navbar to navigate around the app.

# App.js[...]render() {
return (
<Segment>
<Menu>
<Menu.Item name='home'> <Icon name="shop"/></Menu.Item>
<Menu.Item name='Items'/>
<Menu.Item name='aboutUs' />
</Menu>
</Segment>
);
}
}

Now, let’s create an ItemDashboard page that shows all of our Item’s.

#bashtouch src/screens/itemDashboard.js--------------------------------------------------------------------#itemDashboard.jsimport React, { Component } from 'react';
import {Container, Card} from 'semantic-ui-react'
class ItemDashboard extends Component {render() {
return (
<div>
<Container style={{padding: 10}}>
<Card.Group>
<Card>
<Card.Content>
<Card.Header>
Item Name
</Card.Header>
<Card.Meta>
Item Price
</Card.Meta>
<Card.Description>
Description of the Item
</Card.Description>
</Card.Content>
</Card>
</Card.Group>
</Container>
</div>
);
}
}
export default ItemDashboard;

Now we’ll import this into our App.js and render it within our <Segment> section.

# App.js[...]

render() {
return (
<Segment>
<Menu>
<Menu.Item name='home'> <Icon name="shop"/></Menu.Item>
<Menu.Item name='Items'/>
<Menu.Item name='aboutUs' />
</Menu>
<ItemDashboard />
</Segment>
);
}
}

At the moment our app should look nice and basic, something like this.

Basic Item Dashboard

Next, let’s implement a modal, which will serve as our create page.

We’ll use Semantic UI’s Modal and Form components to create the basic outline of our page. Let’s create a new file createItem.js which will live in our screens directory.

#bash$ touch src/screens/createItem.js--------------------------------------------------------------------#createItem.jsimport React, { Component } from 'react';
import { Form, Modal, Button, Container } from 'semantic-ui-react'
class CreateItemModal extends Component {constructor(props) {
super(props)
this.state = {}
this.handleChange = this.handleChange.bind(this);
}
handleChange(event, {name, value}) {
this.setState({ [name]: value });
}
handleSubmit(event) {}handleOpen = () => this.setState({ modalOpen: true })handleClose = () => this.setState({ modalOpen: false })render () {
return (
<Modal trigger={<Button onClick={this.handleOpen}>+ Add Item</Button>} closeIcon={true} open={this.state.modalOpen} onClose={this.handleClose}>
<Modal.Header>Add an Item</Modal.Header>
<Modal.Content>
<Form>
<Form.Group unstackable widths={2}>
<Form.Input name='itemName' label='Item Name' placeholder='Enter Item Name...' onChange={this.handleChange} />
<Form.Input name='itemPrice' label='Item Price' placeholder='Enter Item Price...' onChange={this.handleChange} type='number' />
</Form.Group>
<Form.TextArea name='item_description' label='Item Description' placeholder='Add a Description of the Item...' onChange={this.handleChange} />
<Form.Button type='submit'>Submit</Form.Button>
</Form>
</Modal.Content>
</Modal>
);
}
}
export default CreateItemModal;

Most of the above code is recycled from examples on Semantic’s website. Studying the code, you’ll notice I’ve also added two functions that handleChange and handleSubmit within our form. This ensures all data is captured and submitted correctly, we’ll revisit both of these later when we connect everything together.

One thing I should mention, that I encountered during my own development, is a funky bug with the modal where half of it would be cut, off screen. The issue is tracked here and the solution below worked perfectly, for me. Please do let me know if you run into any funky issues around the modal that aren’t resolved with the below.

Proposed Workaround for Modal Issue:

#App.css.ui.page.modals.transition.visible {
display: flex !important;
}
# suggested by loopmode on GitHub Issues thread.

Make sure to import and add the component in our ItemDashboard.

#src/screens/itemDashboard.js[...]render() {
return (
<div>
<CreateItemModal/>
<Container style={{padding: 10}}>
<Card.Group>
<Card>
<Card.Content>
<Card.Header>
Item Name
</Card.Header>
<Card.Meta>
Item Price
</Card.Meta>
<Card.Description>
Description of the Item
</Card.Description>
</Card.Content>
</Card>
</Card.Group>
</Container>
</div>
);
[...]

The finished createItemModal should look something like the below.

Looking good! Now let’s hook everything up so that we can actually create items that display on our Dashboard.

Create

Now that we have a form we’ll need to populate the function handleSubmit we wrote in earlier.

Similar to our get request previously, we’ll have to set the apiName, path, and this time, the body of what we’re sending. We’ll then use Amplify’s API component to post and log the contents to the console.

#createItem.jsimport Amplify, { API } from 'aws-amplify';class CreateItemModal extends Component {constructor(props) {
super(props)
this.state = {}
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}handleChange(event, {name, value}) {
this.setState({ [name]: value });
}
handleSubmit(event) {
let apiName = 'sampleCloudApi';
let path = '/items';
let newItem = {
body: {
name: this.state.itemName, price: this.state.itemPrice, description: this.state.itemDescription
}
}
API.post(apiName, path, newItem).then(response => {
console.log(response)
}).catch(error => {
console.log(error.response)
});
event.preventDefault();
this.handleClose()
}
handleOpen = () => this.setState({ modalOpen: true })handleClose = () => this.setState({ modalOpen: false })render () {
return (
<Modal trigger={<Button onClick={this.handleOpen}>+ Add Item</Button>} closeIcon={true} open={this.state.modalOpen} onClose={this.handleClose}>
<Modal.Header>Add an Item</Modal.Header>
<Modal.Content>
<Form onSubmit={this.handleSubmit}>
<Form.Group unstackable widths={2}>
<Form.Input name='itemName' label='Item Name' placeholder='Enter Item Name...' onChange={this.handleChange} value={this.state.itemName} />
<Form.Input name='itemPrice' label='Item Price' placeholder='£0.00' onChange={this.handleChange} value={this.state.itemPrice} />
</Form.Group>
<Form.TextArea name='itemDescription' label='Item Description' placeholder='Add a Description of the Item...' onChange={this.handleChange} value={this.state.itemDescription} />
<Form.Button type='submit'>Submit</Form.Button>
</Form>
</Modal.Content>
</Modal>
);
}
}
export default CreateItemModal;

The handleSubmit function declares the variables necessary for posting to our backend. The body is formed of the values we set in each form element: itemName, itemPrice and itemDescription. The last thing we need to do is bind our function to our component so that it doesn’t show up undefined when we call our function.

Now let’s give the form a quick test to see if everything’s working.

After submitting the form, with some content obviously, you should see the contents in the console, like above.

Great! However, this isn’t saving anywhere, we still need to configure our Database.

DynamoDB Setup

Earlier on we setup a Database with the default example setup, we’re going to remove this and configure the database from scratch. DynamoDB requires us to specify a Primary Key, which determines how items within our Table are uniquely organised. There’s a lot of different schools of thought on what’s best to do here, which are way out of scope of this post. To keep things simple I’ll create a UUID, which will be generated when a user submits a new Item using the form.

#bash$ awsmobile database configure? Select from one of the choices below. Remove table from the project
? Select table to be deleted AWSMobileTable
? Are you sure you want to delete the table Yes
$ awsmobile database configureWelcome to NoSQL database wizard
You will be asked a series of questions to help determine how to best construct your NoSQL database table.
? Should the data of this table be open or restricted by user? Open
? Table name ServerlessReactExample
You can now add columns to the table.? What would you like to name this column ID
? Choose the data type string
? Would you like to add another column Yes
? What would you like to name this column ItemName
? Choose the data type string
? Would you like to add another column Yes
? What would you like to name this column ItemPrice
? Choose the data type number
? Would you like to add another column Yes
? What would you like to name this column ItemDescription
? Choose the data type string
? Would you like to add another column No
Before you create the database, you must specify how items in your table are uniquely organized. This is done by specifying a Primary key. The primary key uniquely identifies each item in the table, so that no two items can have the same key.
This could be and individual column or a combination that has "primary key" and a "sort key".
To learn more about primary key:
http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html#HowItWorks.CoreComponents.PrimaryKey
? Select primary key ID
? Select sort key (No Sort Key)
You can optionally add global secondary indexes for this table. These are useful when running queries defined by a different column than the primary key.
To learn more about indexes:
http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html#HowItWorks.CoreComponents.SecondaryIndexes
? Add index No$ awsmobile push
[...]
contents in #current-backend-info/ is synchronized with the latest in the aws cloud

Again, to keep things simple I’ve not added a sort key or an index, just the Primary key which is require for setting up the db.

Let’s quickly tweak our handleSubmit function to generate a uuid whenever we submit. In order to achieve this we’ll use node-uuid. There’s a few different versions you can generate, but we’ll use the uuidv1, which creates a timestamp uuid. This can be converted and tested on a site like this.

#bash $ yarn add node-uuid------------------------------------------------------------------#createItem.jsconst uuidv1 = require('uuid/v1');[...]
handleSubmit(event) {
console.log(this);
let apiName = 'sampleCloudApi';
let path = '/items';
let newItem = {
body: {
ID: uuidv1(), name: this.state.itemName, price: this.state.itemPrice, description: this.state.itemDescription
}
}
API.post(apiName, path, newItem).then(response => {
console.log(response)
}).catch(error => {
console.log(error.response)
});
event.preventDefault();
this.handleClose()
}
[...]

Lastly, we need to configure our cloud-api to use our newly created database.

#bashawsmobile cloud-api configureThis feature will create an API using Amazon API Gateway and AWS Lambda. You can optionally have the lambda function perform CRUD operations against your Amazon DynamoDB table.? Select from one of the choices below. Create CRUD API for an existing Amazon DynamoDB table
? Select Amazon DynamoDB table to connect to a CRUD API ServerlessReactExample
Adding lambda function code on:
/Users/jameshamann/Documents/Development/serverless-web-app-example/awsmobilejs/backend/cloud-api/rverlessReactExample/
...
Path to be used on API for get and remove an object should be like:
/ServerlessReactExample/object/:ID
Path to be used on API for list objects on get method should be like:
/ServerlessReactExample/:ID
JSON to be used as data on put request should be like:
{
"ID": "INSERT VALUE HERE",
"ItemDescription": "INSERT VALUE HERE",
"ItemName": "INSERT VALUE HERE",
"ItemPrice": "INSERT VALUE HERE"
}
To test the api from the command line (after awsmobile push) use this commands
awsmobile cloud-api invoke ServerlessReactExampleCRUD <method> <path> [init]
$ awsmobile push

This does change the name of your API, you’ll notice mine is now called ServerlessReactExampleCRUD, with a path of /ServerlessReactExample. So in order to get things to work, you’ll need to update the apiName and path in your code to reflect this.

Now let’s create a new item in our form and hit Submit. We still don’t see anything, but if we head over to the AWS DynamoDB Console, we’ll see our item has been successfully saved in our database!

Read

Now that we can successfully Create items, let’s work on Reading them in our dashboard. Instead of reading one Item at a time, I’m going to fetch all Items in our database and display them on our itemDashboard. To do this, we’ll need to slightly alter our Lambda code to include a function that returns all items from our DynamoDB Table.

DynamoDB provides us with a .scan method, which returns all items, similar to a SELECT * from tablename in SQL. Let’s start off by editing our app.js within our backend folder and include a route that retrieves all data.

#awsmobilejs/backend/yourLambdaFunctionName/app.js[...]app.get('/ServerlessReactExample', function(req, res) {var params = {
TableName: tableName,
Select: 'ALL_ATTRIBUTES',
};
dynamodb.scan(params, (err, data) => {
if (err) {
res.json({error: 'Could not load items: ' + err.message});
}
res.json({
data: data.Items.map(item => {
return item;
})
});
});
});
[...]-------------------------------------------------------------------#bash$ awsmobile push
[...]

Be sure to add the function just above the other routes and to push your changes to the aws cloud.

Once everything has been pushed and updated, reload your app and, if everything’s setup correctly, you should see all items from the database.

After refreshing

Let’s update our itemDashboard so that it correctly displays the data fetched from the database. We’ll iterate through our JSON response and for each item, we’ll display the name, price and description on a Card. We’ll require another library, lodash, which basically makes working with objects a lot easier.

#bash $ yarn add loadash-------------------------------------------------------------------#src/screens/itemDashboard.jsimport React, { Component } from 'react';
import {Container, Card} from 'semantic-ui-react'
import Amplify, { API } from 'aws-amplify';
import _ from 'lodash';
let apiName = 'ServerlessReactExampleCRUD';
let path = '/ServerlessReactExample';
class ItemDashboard extends Component {constructor(props){
super(props)
this.state = {itemData: {}}
}
getItems(){
API.get(apiName, path).then(response => {
console.log(response)
this.setState({
itemData: response.data
});
});
}
componentDidMount(){
this.getItems()
}
render() {
const itemData = this.state.itemData;
return (
<div>
<CreateItemModal/>
<Container style={{padding: 10}}>
<Card.Group>
{_.map(itemData, ({ID, ItemName, ItemPrice, ItemDescription }) => (
<Card>
<Card.Content>
<Card.Header>
{ItemName}
</Card.Header>
<Card.Meta>
£ {ItemPrice}
</Card.Meta>
<Card.Description>
{ItemDescription}
</Card.Description>
</Card.Content>
</Card>
))}
</Card.Group>
</Container>
</div>
);
}
}
export default ItemDashboard;

We move our GET request into it’s own function, getItems() and call it during the componentDidMount function. Then, in our render function, we set itemData equal to this.state.itemData, which is set during our getItems() function. In the UI, we _.map over our itemData and for each Item we return a Card with the ItemName, ItemPrice and ItemDescription. After reloading your app, the result should look something like the below.

Before moving on, you’ll notice in order to see your results after a form submission you’ll need to refresh. Let’s fix that with a callback and set the state of this.state.itemData to get updated when we submit our form.

#src/screens/itemDashboard.js[...]render() {
const itemData = this.state.itemData;
return (
<div>
<CreateItemModal getItems={this.getItems}/>
<Container style={{padding: 10}}>
<Card.Group>
{_.map(itemData, ({ID, ItemName, ItemPrice, ItemDescription }) => (
<Card>
<Card.Content>
<Card.Header>
{ItemName}
</Card.Header>
<Card.Meta>
£ {ItemPrice}
</Card.Meta>
<Card.Description>
{ItemDescription}
</Card.Description>
</Card.Content>
</Card>
))}
</Card.Group>
</Container>
</div>
);
}
}
[...]-------------------------------------------------------------------
#src/screens/createItem.js
[...]handleSubmit(event) {
console.log(this);
let apiName = 'ServerlessReactExampleCRUD';
let path = '/ServerlessReactExample';
let newItem = {
body: {
"ID": uuidv1(),
"ItemName": this.state.itemName,
"ItemPrice": this.state.itemPrice,
"ItemDescription": this.state.itemDescription
}
}
API.post(apiName, path, newItem).then(response => {
console.log(response)
}).catch(error => {
console.log(error.response)
});
event.preventDefault();
this.props.getItems()
this.handleClose()
}
[...]

We pass the function down from ItemDashboard to createItem.

Update

Next, we’ll look at Updating our Items, if we want to change the price, for example.

Let’s create another modal, exactly the same as our createItem.js, but let’s call it editItem.js. Alot of the contents will be similar as it’s essentially the same form, just for editing an item.

#src/screens/editItem.jsimport React, { Component } from 'react';
import { Form, Modal, Button, Container, Icon } from 'semantic-ui-react'
import Amplify, { API } from 'aws-amplify';
const uuidv1 = require('uuid/v1');
class EditItemModal extends Component {constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.state = { item: this.props.item };
}
handleChange(event, {name, value}) {
this.setState({ [name]: value });
}
handleSubmit(event) {
let apiName = 'ServerlessReactExampleCRUD';
let path = '/ServerlessReactExample';
let editItem = {
body: {
"ID": this.props.item[0].ID,
"ItemName": this.state.itemName,
"ItemPrice": this.state.itemPrice,
"ItemDescription": this.state.itemDescription
}
}
API.put(apiName, path, editItem).then(response => {
console.log(response)
}).catch(error => {
console.log(error.response)
});
this.props.getItems()
this.handleClose()
event.preventDefault();
}
handleOpen = () => this.setState({ modalOpen: true })handleClose = () => this.setState({ modalOpen: false })render () {
return (
<Container style={{padding: 10}}>
<Modal trigger={<Button icon onClick={this.handleOpen}><Icon name='edit' /></Button>}open={this.state.modalOpen} closeIcon
onClose={this.handleClose}>
<Modal.Header>Edit</Modal.Header>
<Modal.Content>
<Form onSubmit={this.handleSubmit}>
<Form.Group unstackable widths={2}>
<Form.Input name='itemName' label='Item Name' placeholder='Edit Item Name...' onChange={this.handleChange} value={this.state.itemName} />
<Form.Input name='itemPrice' label='Item Price' placeholder='£0.00' onChange={this.handleChange} value={this.state.itemPrice} />
</Form.Group>
<Form.TextArea name='itemDescription' label='Item Description' placeholder='Edit Description of the Item...' onChange={this.handleChange} value={this.state.itemDescription} />
<Form.Button type='submit'>Submit</Form.Button>
</Form>
</Modal.Content>
</Modal>
</Container>

);
}
}
export default EditItemModal;

Essentially this is the same code as what’s in createItem.js, the only difference is we’re receiving props we’ve sent from our parent component itemDashboard.js, in order to retrieve the item’s ID. This is important, as we require the ID in order to make sure we’re updating the correct item in our database.

To send the Item to our EditItemModal , we need to create a function that fetches the item. We’ll do this in our ItemDashboard and send the response to our EditItemModal through props.

#src/screens/ItemDashboard.js
[...]
import EditItemModal from './editItem.js'[...]getItem(id){
let single_path = '/ServerlessReactExample/' + id
console.log(single_path)
API.get(apiName, single_path).then(response => {
console.log(response)
this.setState({
item: response
})
});
}
[...]return (
<div>
<Container style={{padding: 10}}>
<Card.Group>
{_.map(itemData, ({ID, ItemName, ItemPrice, ItemDescription }) => (
<Card onClick={() => this.getItem(ID)}>
<Card.Content>
<Card.Header>
{ItemName}
</Card.Header>
<Card.Meta>
£ {ItemPrice}
</Card.Meta>
<Card.Description>
{ItemDescription}
</Card.Description>
</Card.Content>
<EditItemModal item={Object.values(this.state.item) getItems={(this.getItems)} />
</Card>
))}
</Card.Group>
</Container>
</div>
);

Don’t forget to import the EditItemModal, as we’ll be rendering this within our dashboard on each card. Our function, getItem, takes an ID as it’s argument, the ID is then appended to the path used within our GET request, which returns all information on the requested Item. We then set the state of item equal to the response of our call and send that data through to our EditItemModal.

Once Complete, you should have a dashboard similar to this, with the ability to edit the item you’ve clicked on.

Awesome! Lastly, let’s quickly implement our last CRUD function, delete.

Delete

Implementing a delete function is fairly quick, as most of the work is already done. We’ll add the delete function to our EditItemModal, as it’s probably the best place for it.

#src/screens/editItem.js[...]constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.deleteItem = this.deleteItem.bind(this);
this.state = { item: this.props.item };
}
deleteItem(){
let apiName = 'ServerlessReactExampleCRUD';
let path = "/ServerlessReactExample/object/" + this.props.item[0].ID
API.del(apiName, path).then(response => {
console.log(response)
}).catch(error => {
console.log(error.response)
});
this.props.getItems()
this.handleClose()
}
[...]

Very simply, our function makes a delete request, using API component, with the ID appended to the path. The ID is required so the function know’s which Item to delete from our database. Don’t forget to bind our function to this, otherwise we’ll get undefined errors when trying to retrieve our ID.

Lastly, let’s render a Delete button on our Modal and link it all together.

#src/screens/editItem.jsreturn (
<Container style={{padding: 10}}>
<Modal trigger={<Button icon onClick={this.handleOpen}><Icon name='edit' /></Button>} open={this.state.modalOpen} closeIcon onClose={this.handleClose}>
<Modal.Header>Edit</Modal.Header>
<Modal.Content>
<Form onSubmit={this.handleSubmit}>
<Form.Group unstackable widths={2}>
<Form.Input name='itemName' label='Item Name' placeholder='Edit Item Name...' onChange={this.handleChange} value={this.state.itemName} />
<Form.Input name='itemPrice' label='Item Price' placeholder='£0.00' onChange={this.handleChange} value={this.state.itemPrice} />
</Form.Group>
<Form.TextArea name='itemDescription' label='Item Description' placeholder='Edit Description of the Item...' onChange={this.handleChange} value={this.state.itemDescription} />
<Form.Button type='submit'>Submit</Form.Button>
</Form>
</Modal.Content>
<Modal.Actions>
<Button icon labelPosition='left' onClick={this.deleteItem}>
<Icon name='delete' />
Delete Item
</Button>
</Modal.Actions>

</Modal>
</Container>

Once completed, your EditItemModal should look like this. When clicking delete, and after a refresh, you’ll notice the item has been removed.

This has been quite a long post, so well done if you’ve made it this far! You now have a, real basic, Serverless React Web App! There are still a lot of things that need to be cleaned up and refactored, like perhaps adding the Item Title to the EditItemModal, so we know what we’re editing and adding maybe adding some validation to our forms? There’s a lot of scope for improvement and enhancement here, but this gives you a solid foundation to move forward.

The next, and final, part on this mini series will be focused around Adding Authentication, with Amplify, as well as the Deployment and Distribution of your app through AWS.

All source code for the project can be found here.

As always, thanks for reading, hit 👏 if you like what you read and be sure to follow to keep up to date with future posts.

James Hamann

Written by

Software Developer https://jameshamann.com

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade