Week 10 - Uploading and Rendering Files
Refresher: REST APIs
REST APIs are the most common API type, and comprised of many url paths, or “endpoints”.
They are typically structured so that their URLs represent a hierarchy from left to right.
GET /contacts/{contact_id}/profile
- Get a specific user’s profile imagePUT /contacts/{contact_id}/profile
- Get a specific user’s profile image
The PUT HTTP method creates a new resource or replaces a representation of the target resource with the request content.
The difference between PUT and POST is that PUT is idempotent: calling it once is no different from calling it several times successively (there are no side effects).
Refresher: Fetch API Options
The fetch
function provided by javascript accepts two arguments - the URL, and an object containing any options to customize the request.
const response = await fetch("https://example.org/post", options);
The options object can contain accept anything defined in the RequestInit
docs. The options we are focused on are:
headers
: Used to “describe” the requestmethod
:fetch
performsGET
requests by default. To perform other types of requests -POST
/DELETE
/etc - we pass the method we want as a string.body
: The body of the request, usually containing data, forPOST/PUT/PATCH
requests
Uploading a File with fetch
To upload a file with fetch
, we can use the special FormData
object to let the browser handle the encoding for us. This will include the filename and other metadata automatically.
First, we use a file input to get access to a special File object.
export default function FileUpload() {
function onChange(e) {
console.log('uploaded files', e.target.files, e.target.value)
// output:
// text e.target.files e.target.value
// vvvvvvvvvvvvvv vvvvvvvvvvvvvvvvvvvvvvvvvvvvv vvvvvvvvvvvvvvvvvvvvvv
// uploaded files FileList {0: File, length: 1} C:\fakepath\MASCOT.png
}
return <Input onChange={onChange} type="file"> /</Input>
}
A few things to note here:
- We must pass
type="file"
to an input field to make a file upload e.target.files
is always a list, although by default, only one file can be selectede.target.value
is a string, representing the real filename and a fake path on disk:C:\fakepath\MASCOT.png
- The value here is relatively useless, but we can reset the file input by setting
e.target.value = null
. If we don’t reset the input, the file input will not trigger a change event if the user selects the same file again.
- The value here is relatively useless, but we can reset the file input by setting
Extracting the file with const file = e.target.files[0]
, we have access to a single file upload. We need to add this to a FormData object to serialize it for a network request. We initialize a FormData with new FormData()
, then use .append
to add the file object to the FormData object:
export async function uploadProfilePicture(contactId, file) {
let data = new FormData()
data.append('file', file)
const response = await fetch(`...`, {
headers: { /* ... */ },
method: 'PUT',
// we can include FormData right in the call to fetch(), no need to serialize it ourselves
body: data
});
// remember to check for any errors!
if (!response.ok) {
console.error(`Response status: ${response.status}`);
throw new Error(`Response status: ${response.status}`)
}
return await response.json();
}
Lab - Final Project: Upload and Display Profile Images
For Lab, you’ll be working on the ability to upload profile images for contacts, and displaying them on the contact detail page.
Note: You cannot modify “public contacts” with IDs 1000 - 1100. To make this lab easier, you may want to remove public contacts from the list by including
?public=false
in your call toGET /contacts
1. Display the Contact Profile Image
First, we’ll want to display the contact’s profile image when displaying the contact itself. Just like how we can display an image from Google, we can display an image from our REST API. We only need to set the src
attribute of an img tag to the appropriate URL.
<img src={`https://api.ci256.cloud/contacts/${contactId}/profile`} alt="Contact Profile" />
We’ll want to include an alt
tag incase the browser is unable to load the image.
You’ll know this is working if:
- Loading a public contact displays a placeholder with some initials
2. Handling Contacts without an Image
We may wish to handle when a contact does not have a contact image. The contact object has the attribute profileImage
, which will be true
when the contact has an image, and false
when they do not.
The Avatar
component from @mui/joy
can handle displaying a placeholder, as well as styling the image into a circle.
// only set the `src` attribute when `contact.profileImage` is true
<Avatar src={contact.profileImage && `${API_URL}/contacts/${contact.id}/profile`} />
You’ll know if this is working if:
- Clicking on a contact without a profile image:
- Results in no errors in the console or network tab
- Displays a placeholder image when a contact image does not exist
- Clicking on a public contact displays the appropriate image
3. Uploading a Profile Image
Now we’ll want to add the ability to upload a profile image for each of our contacts.
3A: File Upload Input
First we need to create a file upload input inside our EditContact
and/or CreateContact
component. As discussed above, create an Input
component and set the prop type="file"
. Include an onChange
listener to handle the file upload once selected by the user.
function handleFileUpload(e) {
console.log('uploaded files', e.target.files)
const file = e.target.files[0]
}
<Input onChange={handleFileUpload} type="file" />
You’ll know if this is working if:
- An input displaying “choose file” appears on the screen
- Clicking the button opens a file picker
- Selecting a file results in a console message with file info, and no errors
3B: Handling Cancelled Uploads
If a user clicks the “upload” button, cancels the upload and does not select a file, you may still get an onChange
event with e.target.files
being an empty list.
Handle this edge case inside the onChange
handler, ensuring no errors occur in the console.
function handleFileUpload(e) {
// todo add code to handle when e.target.files is empty
console.log('uploaded files', e.target.files)
const file = e.target.files[0]
}
You’ll know if this is working if:
- Clicking on “choose file”, then clicking X instead of selecting a file, does not result in a console error.
3C: Upload the File
Add a function to contacts.ts
for performing the actual file upload, and call it from your onChange
handler. Remember, you need to:
- Make a
PUT
request to/contacts/{contactId}/profile
- Add the
file
object to a FormData object - Include the FormData object in your request body
- Include appropriate authorization headers!
- Note: You should NOT include the
content-type
header
- Note: You should NOT include the
The function should follow this general pattern:
export async function uploadProfilePicture(contactId, file) {
let data = new FormData()
data.append('file', file)
const response = await fetch(`...`, {
headers: { /* ... */ },
method: 'PUT',
// we can include FormData right in the call to fetch(), no need to serialize it ourselves
body: data
});
// remember to check for any errors!
if (!response.ok) {
console.error(`Response status: ${response.status}`);
throw new Error(`Response status: ${response.status}`)
}
return await response.json();
}
You’ll know if this is working if:
- Clicking “choose a file” and uploading the file results in a network request to
PUT /contacts/{contactId}/profile
- The request returns a 200 OK status
- This is easiest verified by checking if the request in the network tab is red.
- No console errors are present
3D: Update the UI
The UI on the edit page should be updating to display the new image after upload. This is done easiest with a useState
for the image URL, which can easily be updated after file upload.
Note: Due to the url not actually changing, we need to first set the avatar to undefined, then set it back to the new value, so that React sees an update
const [avatar, setAvatar] = useState(contact ? `${API_URL}/contacts/${contact.id}/profile`: undefined)
async function handleFileUpload(e) {
// ...
// other stuff
// ...
// set Avatar to undefined while we make the request
// setAvatar(undefined)
// call function form step 3C
await uploadProfilePicture(contact.id, file)
// update avatar useState to render the new image from the remote server
setAvatar(`${API_URL}/contacts/${contact.id}/profile`)
}
You’ll know if this is working if:
- Uploading a new profile image results in the new image being displayed on the edit page after upload