Uploading file to server with React-hooks, Redux and Multer: PART 1
I started building a portfolio project(book uploads) which started well until I got to the part where I wanted to implement an image and text upload at the front-end part. It took me several hours of googling, and in-person help to finally get it done.
One of the problems I encountered was that there aren’t enough resources(even tho’ I found none) for file and text uploading using react-redux, and that’s why I came up with the idea of writing a doc on it for those who need it.
I will not be explaining how to setup multer in this blog, but I’ll give a video reference(which I learnt from), made by Max(Acadmind). Learn the back-end aspect from it(tweak it to your taste) and let’s move on.
I’m sure you have setup your react form component and if you haven’t, feel free to copy from my code.
All underlined texts are links to resources where you can read more.
import React, { Fragment, useState } from "react";
import PropTypes from "prop-types";
import { withRouter } from "react-router-dom";
import { connect } from "react-redux";
import { createBook } from "../../actions/collection";const CreateBook = ({ createBook, history }) => {
...
}CreateBook.propTypes = { createBook: PropTypes.func.isRequired};export default connect(null, { createBook }(withRouter(CreateBook));const [formData, setFormData] = useState({
title: " "
author: " "
});const [image, setImage] = useState("");
const [imageName, setImageName] = useState("Choose file");const { title, author } = formData;const onFileChange = (e) => {
setImage(e.target.files[0]);
setImageName(e.target.files[0].name);
};const onChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};const onSubmit = (e) => {
e.preventDefault();
const payload = new FormData();
payload.append("bookImage", image);
payload.append("title", formData.title);
payload.append("author", formData.author); createBook(payload, history);
};
So, I first created a state for my text input, then for my image itself, then next is the state to hold the image name(example: json.png ). The following line, I extracted the text input value(title, author, etc) from the state by destructing formData so that I can use them anywhere in my component.
Next, I needed to access the image file, so I used file API added to DOM in HTML5. The e.target.files[0]
is to get the file a user selected, while the e.target.files[0].name
is to get the name of the selected file. So I set the state of image I created(initially empty) to the image user selected, and the filename state(initially Choose file) to the file name of that file the user selected, as simple as that. Then I ensured they are inside the function onFileChange
which I want to call when a user clicks on the file-input(html, as you will see.)
Next, I needed a way to save the user’s input into my state, so the logic behind this is that I set the state(originally empty strings) to the user’s input. To further understand this, let’s use the onChange
handler to explain this.
setFormData({ ...formData, [e.target.name]: e.target.value });
What’s going on here is this: I’m setting the state of the formData
setFormData
to, first the value of the state before any update by the user, this is done by using the javascript spread operator …
and this will copy everything initially in the state, then secondly we want to update that copied state with the user input(whose logic I will explain later on). The reason why we are copying the formData
and updating the copied state data rather than using push
or concat
is because state in React are immutable(they cannot be updated directly), so we need to copy the data in the state(formData
in our case) into a setter I’ll say(setFormData
) which is where we can tamper(update, delete…) with the data. Read more on setting state
Following is the onSubmit
function, which handles user data upon submission. This serves as a form action. The first line of code there e.preventDefault()
was added to prevent the data from refreshing(going empty) upon submission. In other cases, what it does is just to prevent the original action of an event from occurring. Now, what is the next line all about? This is where the real action begins.
I’ll try to explain what FormData()
is as basic as I can. Basically, when you have a form that contains both text and file input, you want to use it, why? This is because it allows you to set the value of an input to the value of its state. To get a better explanation, you can read more from MDN.
It is needed for you to know that when I was creating a payload.append
for the image file, you’ll notice that we don’t have bookImage
defined anywhere in our component, right? That’s actually because it isn’t, we have defined in our back-end. How? If you remember, whenever we use multer, we always have upload.single
when attaching the image to the router.post
or app.post
based on which you are using. So within my router.post
, I had upload.single
, and I passed a parameter of bookImage
within it. As in
router.post("/addBook", upload.single("bookImage"),...)
The next line createBook()
is a redux action, which will be discussed in the second part of this blog. All I’m doing within it is passing the payload
variable created(which holds the action within new FormData()
) and history
(also from redux). Now for the form.
return (
<Fragment>
<form onSubmit={ (e) => onSubmit(e) }
encType="multipart/form-data"> <input type="file" onChange={(e) => onFileChange(e)}
accept="image/*" multiple />
<label htmlFor="title" > Title </label> <input type="text" placeholder="Title here" name="title"
value={title} onChange={(e) => onChange(e)} /> <input type="text" placeholder="Author here" name="author"
value={author} onChange={(e) => onChange(e)} /> <button> Create Book </button> </form>
</Fragment>
);
};
Fragment, a common pattern in React is for a component to return multiple elements. It can be used as a parent wrapper instead of div(in some cases), to better understand why and where you should use it, please reference the docs from React website
Within the form
opening tag, we are saying that upon form submission, we want you to use the onSubmit
function logic which we created earlier to process the submitted data. The enctype
attribute specifies how the form-data should be encoded when submitting it to the server. And the multipart/form-data
means no characters are encoded. This value is required when you are using forms that have a file upload control
We are telling our first input to accept a file, and whenever there is a change(when the user selects a file), we need you to trigger the actions within the onChange
function handler.
For the next two input, we are setting it to accept text as its input type. For the name and value they must be the same because that is what we will use to update the state data(setFormData
), and for the onChange
, whenever there is a change(when the user input a text), we trigger the actions within the onChange
function handler. Now, the reason why we have the name and the value in these two input each(title, author) the same is because of ease and simplicity brought alive by [e.target.name]: e.target.value
. What this means is that instead of the traditional way of setting what the user entered to the value manually, and then setting the state to the value(which now holds the user input), we can simply attach a similar name to the state, name and value, and then attach the value to the name, which is then added to the state.
Next up is the final part of the section, the prop-types
and export
. The prop-types
is where we define of what type
createBook is (could be boolean, function, object…).
CreateBook.propTypes = { createBook: PropTypes.func.isRequired };export default connect(null, { createBook }(withRouter(CreateBook));
The last part export
uses connect
which is a redux callback function, you can read more about it.
The PART 2 of this book will be talking about the redux aspect for this project.