One of the requirements for my final project at Flatiron School was to incorporate something that had not previously been taught in the coursework. Due to my love for beautiful design, I chose image uploading to complete this requirement. Thinking through the request-response cycle, I needed to learn how to select an image through the file browser, how to upload the image through a POST or PATCH request, how to store the image in the database, and how to return the image with a GET request.
Setup
Before we go any further, my app is built with:
- React v18.2.0
- Ruby v2.7.4
- Rails v7.0.5
File Selection
While I actually started with the backend in my own learning process, I thought it made sense to present this tutorial in the order of the request-response cycle.
In addition to being able to access the file browser to select an image to upload, I also wanted to be able to drag'n'drop that image as well. While researching a few options, I came across react-dropzone, a "Simple React hook to create a HTML5-compliant drag'n'drop zone for files." Installation was easy, and the docs were well-written with many examples of different applications.
Initially, I had hoped react-dropzone was an uploader as well, but upon further reading of the docs, I discovered it was not. The docs do recommend other uploaders, but as I learned soon after, uploading in React is super easy.
I created a DropZone component to be used in various forms throughout my application. This component receives a setState
prop used in the useCallback
hook. While the DropZone can handle files of any type, I customized mine to accept images only.
import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
function DropZone({ setState }) {
const onDrop = useCallback(acceptedFiles => {
setState(acceptedFiles[0])
}, []);
const {getRootProps, getInputProps, isDragActive} = useDropzone({
onDrop,
accept: {'image/*': []}
});
return (
<div {...getRootProps()} className={isDragActive ? 'active' : ""}>
<input {...getInputProps()} />
{
isDragActive
? <p>Drop the file here ...</p>
: <p>Drag 'n' drop an image here—or click to select file</p>
}
</div>
);
}
export default DropZone;
This is a simple use of react-dropzone. It has many more options including the ability to preview the image after it's been loaded in state.
With the image file selected and successfully in state, the next objective was to upload it to the server.
Attach & Fetch
This step took a bit of research. With a normal POST or PATCH request, I would use JSON.stringify()
in the body of the request. However, when it came to attaching the file, that solution was unsuccessful.
Instead, I used the FormData constructor and its append() method to attach the image file.
function handleImgSubmit(e, setErrors, form, img, setUser) {
e.preventDefault();
setErrors();
const profile = new FormData();
profile.append('first_name', form.firstName);
profile.append('last_name', form.lastName);
profile.append('phone', form.phone);
profile.append('city', form.city);
profile.append('state', form.state);
profile.append('bio', form.bio);
profile.append('venue_id', form.venueId);
profile.append('video_url', form.videoUrl);
if(img) {
profile.append('avatar', img);
}
// Important! Do not add headers to fetch requests when using FormData()
fetch("/profiles", {
method: "POST",
body: profile
})
.then(r => {
if(r.ok) {
r.json().then((data) => { setUser(data) });
} else {
r.json().then((err) => setErrors(err.errors));
}
});
}
This method works flawlessly when sending files to the backend. Notice the fetch request does not include headers
, or JSON.stringify()
. The FormData constructor takes care of both of these concerns and submitting the fetch request with headers
will result in a server error. FormData can be used with any request whether a file is attached or not. It saves a bit of coding when refactored like this example.
const data = new FormData();
Object.keys(form).map(key => data.append(key, form[key]));
With the frontend complete, the next step is to create a place for the file to land.
Managing the Database
There are many different ways to manage file storage on the backend. Since my backend is a Rails API, I thought it made sense to use Active Storage. As stated in the docs, image processing transformation requires third-party software and an image_processing
gem. Active storage allows easy configuration for online storage services offered through Amazon, Google, or Microsoft Azure.
In order to associate an image with a record, use the has_one_attached
macro.
class Profile < ApplicationRecord
belongs_to :user
belongs_to :venue
has_one_attached :avatar
end
Rails 6.0+ also allows for a migration to include attachment
as the attribute type. rails generate model Profile avatar:attachment
In the controller, call avatar.attach
to attach the image file to the profile.
class ProfilesController < ApplicationController
def create
profile = @current_user.build_profile(profile_params) if @current_user.profile.nil?
profile.avatar.attach(profile_params[:avatar]) unless profile_params[:avatar].nil?
venue = Venue.find(profile_params[:venue_id])
venue.profiles << profile
profile.save!
render json: @current_user, status: :created
end
private
def profile_params
params.permit(:avatar, :first_name, :last_name, :bio, :phone, :city, :state, :venue_id, :video_url)
end
end
Now, the image file is attached and associated with the proper record. How do we send it back to the frontend?
The Response
Once the file has been uploaded to the server, it’s time to send it back to the frontend. What actually gets sent to the front end is not the file, but rather, a link to the file’s location on the server. Working with the link on the front end is just like working with a link to any other URL.
Rails includes a helper to access the URL for the attached image file. First include
the helper in the class. Then create a private method using the url_for
helper to access the image file's URL.
class ProfileSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
attributes :id, :avatar, :first_name, :last_name, :phone, :city, :state, :bio, :video_url
has_one :user
has_one :venue
private
def avatar
url_for(object.avatar) if object.avatar.attached?
end
end
Now the URL for the image file will be serialized when the controller renders a JSON.
Conclusion
So that's it! From front to back and back to front, working with image files is relatively straightforward. Utilizing React hooks like react-dropzone makes it very simple to put together a slick file selector. The FormData constructor handles the frontend attaching, and Active Storage manages the server-side associations and access. Returning the image is now simply returning the URL for the image. Hope this blog helps. Happy coding!
Top comments (0)