AWS Amplify for React/React Native Development — Pt 5 Uploads

Val Nuccio
9 min readMar 2, 2021

Welcome back everyone! If you have been following along in my AWS Amplify series so far we have:

  • Configured an Amplify CLI
  • Created a “To Do List” React Application initialized with Amplify and protected behind Amplify Authentication
  • Served up a GraphQL API on AppSync with a corresponding Database on DynamoDB that connected our ToDo information with….
  • Corresponding JPG files conveniently stored in AWS S3.

All of this was done by creating mutations and uploading files directly into the AWS Management Console which gave us a lovely little tour around the AWS Services Website.

Now this is all well and good but we can’t imagine that an user on this Website would want to head into AWS and muck about in all that. (Also.. we definitely wouldn’t want them in there messing with it….)

SO that means we have to add the functionality of adding an upload to the database and storage bucket right into our website. Let’s get started!

The great thing about the point we are currently at in our application is that we don’t need to worry about implement any new technologies today. We have already worked with al the AWS elements we are going to use today. That being said we will have a lot of steps to cover so the best way to proceed to is create a plan of action.

PLAN OF ACTION

Step 1 -

  • create a Form that gives us input fields required to both create an entry in our database and associate that entry with a corresponding file in our S3 bucket.

Step 2 -

  • Gather all of those inputs and organize them into the appropriate functional calls to AWS.

Step 3-

  • Adjust Rendering to account for those changes.

STEP ONE

Create the form

Ok so we know that we are going to need to add a Form onto this page. We know that we are going to need a few different entries on this form in order to populate the schema we previously created. But do we remember what we are going to need?

If you don’t then go ahead and check out your schema.graphql file — If you recall it should look like this:

type Todo @model {id: ID!name: String!description: StringfilePath: String}

Pretty straightforward. Another way to verify what we are going to need is to go ahead and check out the code generated for us in the src/graphql folder. In there we have all the different ways that we can query (request information from) or mutate (aka “mutations”) (i.e. make changes to) our database.

For our purposes the mutation we need is pretty straightforward and should be called “createTodo”. It looks like this:

export const createTodo = /* GraphQL */ `mutation CreateTodo($input: CreateTodoInput!$condition: ModelTodoConditionInput) {createTodo(input: $input, condition: $condition) {idnamedescriptionfilePathcreatedAtupdatedAt}}`;

In this mutation you will see a list of required inputs we need when creating a Todo. In our case the Id, createdAt, and updatedAt will be created automatically by AWS if we don’t list anything so all we need to do is create values for name, description, and filePath.

so a theoretical form component might look something like:

const AddToDoForm = ()=>{const [toDoName, setToDoName] = useState(‘’)const [toDoDescription, setToDoDescription] = useState(‘’)const [jpgData, setJpgData] = useState()return(<form onSubmit={(e)=>{e.preventDefault();}}><input type={‘text’} value={toDoName} onChange={(e)=>setToDoName(e.target.value)}/><input type={‘text’} value={toDoDescription} onChange={(e)=>setToDoDescription(e.target.value)}/><input type={‘submit’}/></form>)}

There is your basic controlled form with an input field for name and description. However, for our purposed we need to upload a file right? For that we need to add a new kind of input specifically designed to upload files! Ours will accept different types of image files and on the selection of the upload will send that file to a new state that we will add to the top of our form.

<input type={‘file’} accept=”image/png, image/jpeg” onChange={(e)=> setJpgData(e.target.files[0])}/>

Meanwhile at the top of the Component lets add a new useState to receive that:

const [jpgData, setJpgData] = useState()

Great! We now have a form that can set those values as State for our uploading purposes!

Step 2

Create the Uploads

So what am I saying when I say we are going to create the uploads? I am saying we are going to organize the information we have into calls to both our S3 Storage bucket and also to our Graphql Api. To do this we are going to create a function that is called upon the submit of the form that will do all the work for us!

Let’s create a function called uploadImage() that will receive the information passed in upon the submission of the form.

This function is going to have a few requirements.

  • Firstly it is going to live inside the form Component so it has access to all the local State that we just defined.
  • Secondly it is going to be an async/await function because of the fetches it will be sending.
  • We want to send the file to our S3 bucket first so we can have that file information as our filePath for when we …
  • Submit that information to our GraphQL Api

So lets define the function

const uploadImage = async () =>{const {key} = await Storage.put(`${uuid()}.mp3`, jpgData, {contentType:’image/png, image/jpeg’})const createImgObj= {id:uuid(),name:toDoName,description: toDoDescription,filePath: key}await API.graphql(graphqlOperation(createTodo, {input:createImgObj})}

So let’s talk about what just happened. Firstly, we set a constant of {key} that awaits for the response from our put to Storage. A Storage. put requires three things:

  • a filename.
  • the data content
  • content type

For the S3 bucket you want to make sure you use a unique identifier rather than necessarily a file name. This is because if you were to pass in a regular name it and there happened to be another file with the same name in the bucket then it would overwrite it! One great resource for automatically creating unique identifiers is UUID. For the data content we are passing in the content we saved to our state from our actual file upload. Lastly, we have the content type we also used in our image upload.

***If you plan on using this make sure to run npm i uuid and import it at the top like so:

import {v4 as uuid} from ‘uuid’;

UUID has different versions and you import it using those names which is why it has a bit of an odd import. I’m using the renaming convention in React to be able to refer to it by uuid in our function.

Read more about UUID here:

Universally unique identifier

A universally unique identifier ( UUID) is a 128-bit number used to identify information in computer systems. The term…en.wikipedia.org

Next we create an object that we can pass into our GraphQl mutation! For the name and description we are passing in the values from State that we assigned in our form, and then using the key returned from Storage we defined immediately above.

Lastly we send a mutation to the database. For this format what we need use is a function provided by Amplify that takes in a mutation from our mutations file with the accompanying input. This “createTodo” is the same mutation I mentioned before from our mutations file! Because it is defined in another file we have to make sure we import it at the top the file with :

import {createTodo} from ‘./graphql/mutations’

Step 3

Correcting the Render

Now for the odds and ends!

Lets create a button that upon clicking will show the form component that we already created!

Create a useState that will allow you to toggle the boolean values of whether your form is showing or not with an initial value of false.

const [showForm, setShowForm] = useState(false)

and In your return statement add a ternary expression that responds to those values

{showForm?<AddToDoForm />:<button onClick={()=>{setShowForm(true)}}>Click to Add To Do</button>}

Finally, we want to pass in a prop from our App component to our form component that will allow us to re-render the page and pass up the new information once we that upload has completed.

In order to re-render the page we will want a few things to happen once the upload is complete. We want to hide the form again so we want it to setShowForm(false).

Next we want to send a new fetch request to get the updated list of To Do items with getList(). This can all be triggered by passing down a prop that manages triggers that sequence of events and calling it at the very end of our Image Upload. This looks like :

<AddToDoForm onUpload={(e)=>{setShowForm(false);getList()}}/>

Then you just have to receive it as props in your AddToDoForm component and call it with onUpload() (or props.onUpload() if you don’t destructure it) in the last like of our uploadImage() function.

BUT WAIT — we are gonna have a bug! Did you catch it? Our list is going to re-render as soon as we call getList() because both of our expressions evaluate to true at :

{list && listUrls? <div>{renderList()}</div>:null}So let’s add one more condition to that onUpload!<AddToDoForm onUpload={(e)=>{setShowForm(false);setListUrls(false)getList()}}/>

Now the render will wait until we have fully fetched all our new images as well.

Check out the full code below:

import logo from ‘./logo.svg’;import ‘./App.css’;import {v4 as uuid} from ‘uuid’;import React, {useState, useEffect} from ‘react’;import {API, graphqlOperation, Amplify, Storage} from ‘aws-amplify’;import awsconfig from ‘./aws-exports’;import { withAuthenticator, AmplifySignOut } from ‘@aws-amplify/ui-react’;import {createTodo} from ‘./graphql/mutations’Amplify.configure(awsconfig)//used to query for the list Itemsconst listItems = `query MyQuery {listTodos {items {idnamedescriptionfilePath}}}`// used for rendering each itemconst ListItem = ({id, name, description, imageUrl}) =>{return (<><h3>{name}</h3><p>{description}</p><img src={imageUrl}/></>)}const AddToDoForm = ({onUpload})=>{const [toDoName, setToDoName] = useState(‘’)const [toDoDescription, setToDoDescription] = useState(‘’)const [jpgData, setJpgData] = useState()const uploadImage = async () =>{//sends the put to the Storage service and saves the response in a keyconst {key} = await Storage.put(`${uuid()}.mp3`, jpgData, {contentType:’image/png, image/jpeg’})//creates an input in the right format for being sent to the APIconst createImgObj = {id:uuid(),name:toDoName,description: toDoDescription,filePath: key}//sends the mutation to the API and awaits response before proceedingawait API.graphql(graphqlOperation(createTodo, {input:createImgObj}))//triggers passed down prop that will effect renderonUpload()}return(<form onSubmit={(e)=>{e.preventDefault();uploadImage()}}><input type={‘text’} value={toDoName} onChange={(e)=>setToDoName(e.target.value)}/><input type={‘text’} value={toDoDescription} onChange={(e)=>setToDoDescription(e.target.value)}/><input type={‘file’} accept=”image/png, image/jpeg” onChange={(e)=> setJpgData(e.target.files[0])}/><input type={‘submit’}/></form>)}function App() {// this is setup for the ‘fetch’ essentiallyconst[list, setListItems] = useState(null)const[listUrls, setListUrls] = useState(null)const [showForm, setShowForm] = useState(false)// this has to be an async request so it waitsconst getImages = async items => {let urlArray=[]for(let index=0; index<items.length;index++){const toDoFilePath=items[index].filePathconst toDoAccessUrl= await Storage.get(toDoFilePath)const imageObj={id:items[index].id, url: toDoAccessUrl}urlArray.push(imageObj)}setListUrls(urlArray)}//this is the fetch to the databaseconst getList = async() => {// The API.graphql call here is what we imported aboveconst { data } = await API.graphql(// this is also imported above and we are passing in our query requirementsgraphqlOperation(listItems));// this is basically saying after you get that back to do this with that data// IMPORTANT NOTE : you are setting the listItems of the useState here. in order to avoid null items I’ve incorporated the question marks.// When you make that initial query you are going to get all the info back you would if you were in the aws console. Go back and take a look at that structure// to see why and how I only wanted the items themselves below.setListItems(data?.listTodos?.items)getImages(data?.listTodos?.items)}useEffect(()=>{// calls this once its done rendering the basic pagegetList()},[])const renderList = () =>{return list.map((item)=>{let obj=listUrls.filter((ele)=> ele.id===item.id)return (<ListItem id={item.id} name={item.name} description={item.description} imageUrl={obj[0].url} filePath={item.filePath}/>)})}return (<div className=”App”><header className=”App-header”>{/* This renders the first time with nothing under it */}<h1>To Do List</h1>{/* only renders once the info has been set! */}{list && listUrls? <div>{renderList()}</div>:null}{showForm?<AddToDoForm onUpload={(e)=>{setShowForm(false);setListUrls(false)getList()}}/>: <button onClick={()=>{setShowForm(true)}}>Click to Add To Do</button>}<div><AmplifySignOut/></div></header></div>);}export default withAuthenticator(App);

--

--