Creating a React Component That Allows Csv and Txt File Upload
While working on a React project, I implemented a responsive file upload component that supports drag and drib without using any libraries. Most of the file upload components online used libraries such as react-dropzone to back up drag and drop. So, I thought I'd share how I made the component and testify a typical apply example for it.
End result
The features include:
- elevate and drop without using any libraries
- displaying epitome preview for paradigm files
- displaying file size & name
- removing files in the "To Upload" department
- preventing user from uploading files bigger than a specified size
- Note: this should likewise exist done on the backend for security reasons
Project Setup
Prerequisite: Node (for installing npm packages)
If you are familiar with building React applications, the easiest way to set up a new React projection is by using create-react-app. And then, run the following commands in a terminal/command-line:
npx create-react-app react-file-upload cd react-file-upload
To ensure everything was ready properly after you run npm outset
, the following should appear once you lot visit localhost:3000
in a browser:
Before building the component, allow'south change and remove some files to get rid of unnecessary lawmaking.
- Modify
App.js
to the post-obit:
import React from 'react'; function App() { return ( <div></div> ); } export default App;
- Alter
index.js
to the following:
import React from 'react'; import ReactDOM from 'react-dom'; import './alphabetize.css'; import App from './App'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') );
Remove all files in the src
folder except
-
App.js
-
alphabetize.js
-
index.css
File Upload Component
Installing Dependencies
The dependencies we will demand are:
styled-components
- For styling the component
- styled components allow for mode encapsulation and creating dynamic styles via props
node-sass
- For compiling Sass styles used in styled components (Optional, can use CSS)
To install them, run npm i styled-components node-sass
.
Folder Construction
A adept convention for structuring folders and files is to create a components folder that has a folder for each component. This makes it easier to find the logic and styles for each component.
Following this convention, create a components folder in the src
folder and so a file-upload folder inside the components
folder.
Lastly, within the file-upload folder, create 2 new files.
-
file-upload.component.jsx
-
file-upload.styles.js
Land
Since nosotros are creating a functional component and need to use state, we will use the useState claw.
The useState claw returns a stateful value which is the same as the value passed every bit the outset statement, and a role to update information technology.
For our purposes, we will demand state to continue track of the uploaded files. And so, in the file-upload.component.jsx
file, add the post-obit:
import React, { useState } from "react"; const FileUpload = () => { const [files, setFiles] = useState({}); render ( <div></div> ) } export default FileUpload;
"Shouldn't we utilize an empty array instead of an empty object for the files
state?"
Using an object volition allow usa to easily manipulate (add/remove) the files
state and forestall files with the same proper noun from being uploaded more than in one case. Hither is an example of how the files
state will look like:
{ "file1.png": File, "file2.png": File }
If we used an array it would crave more work. For instance, to remove a file we would have to iterate through each file until we find the one to remove.
Note: File is a JS object. More info tin can exist constitute at https://developer.mozilla.org/en-US/docs/Web/API/File.
useRef hook
If you expect at Figure 1 above, you volition notice the user can either drag and drop files or press the Upload Files button. By default, an file input tag will open the file explorer once it is clicked. Even so, we want to open it once the Upload Files push button is clicked and so we will crave a DOM reference to the file input tag.
To create a DOM reference, we will apply the useRef claw. The useRef hook returns a mutable ref object whose .current
property refers to a DOM node (file input tag in this instance).
In one case we apply the useRef hook, we must laissez passer the returned value to the ref attribute of the file input tag, similar so:
import React, { useState, useRef } from "react"; const FileUpload = (props) => { const fileInputField = useRef(null); const [files, setFiles] = useState({}); render ( <input type="file" ref={fileInputField} /> ) } export default FileUpload;
Props
The component will take the following props:
-
characterization
- Determines the label of the component (e.grand. "Contour Image(s)" in Figure 1 above)
-
maxFileSizeInBytes
- For preventing files above the specified size from being uploaded
-
updateFilesCb
- A callback office used for sending the
files
country to the parent component
- A callback office used for sending the
"Why do we need to transport the files
land to the parent component?"
Typically, the file upload component will be used in a form and when working with forms in React, the component stores the form data in the state. Thus, for the parent component to also store the uploaded files, we demand the file upload component to send it.
"Why do we need use a callback function to send the files
land to the parent component?"
Since React has unidirectional data menses, we cannot easily pass data from the child component (file upload component) to the parent component. Every bit a workaround, nosotros volition laissez passer a function declared in the parent component and the file upload component will telephone call that function with the files
country every bit an argument. This process of sending information from the child to the parent can be further explained at https://medium.com/@jasminegump/passing-data-between-a-parent-and-child-in-react-deea2ec8e654.
Using destructuring, we can now add together the props like then:
import React, { useRef, useState } from "react"; const DEFAULT_MAX_FILE_SIZE_IN_BYTES = 500000; const FileUpload = ({ label, updateFilesCb, maxFileSizeInBytes = DEFAULT_MAX_FILE_SIZE_IN_BYTES, ...otherProps }) => { const fileInputField = useRef(null); const [files, setFiles] = useState({}); return ( <input blazon="file" ref={fileInputField} /> ) } export default FileUpload;
"Why are we using the spread syntax when destructuring otherProps
?"
When destructuring, nosotros can assign all other values that were not explicitly destructured to a variable.
allow props = { a: one, b: 2, c: 3}; allow {a, ...otherProps} = props; //a = 1 //otherProps = {b: 2, c: 3};
In this instance, for any props that we do not destructure, they volition be assigned to the otherProps
variable. We will see the utilize of this otherProps
variable later.
HTML
For the icons shown in Figure ane, nosotros volition be using Font Awesome. To import it, add together the following in the head tag in the public/alphabetize.html
file:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-crawly/5.13.0/css/all.min.css" />
From Effigy 1, it is evident nosotros can split the HTML for the component into 2 main parts.
Hither is the component with the HTML for the kickoff office:
import React, { useRef, useState } from "react"; const DEFAULT_MAX_FILE_SIZE_IN_BYTES = 500000; const FileUpload = ({ label, updateFilesCb, maxFileSizeInBytes = DEFAULT_MAX_FILE_SIZE_IN_BYTES, ...otherProps }) => { const fileInputField = useRef(nix); const [files, setFiles] = useState({}); render ( <section> <label>{label}</label> <p>Drag and driblet your files anywhere or</p> <push button type="push"> <i className="fas fa-file-upload" /> <span> Upload {otherProps.multiple ? "files" : "a file"}</span> </button> <input type="file" ref={fileInputField} title="" value="" {...otherProps} /> </section> ); } consign default FileUpload;
Earlier, we discussed that any props that we don't destructure will be assigned to the otherProps
variable (i.due east. any prop other than label
, updateFilesCb
, maxFileSizeInBytes
). In the code above, we are taking that otherProps
variable and passing it to the file input tag. This was washed and then that we tin can add together attributes to the file input tag from the parent component via props.
"Why are nosotros setting the championship and value attribute to ""
?"
Setting the championship aspect to ""
gets rid of the text that shows up past default when hovering over the input tag ("No file chosen").
Setting the value aspect to ""
fixes an edge case where uploading a file right subsequently removing it does not alter the files
state. After, nosotros will see that the files
state only changes once the value of the input tag changes. This issues occurs because when nosotros remove a file, the input tag'southward value does not change. Since land changes re-render HTML, setting the value aspect to ""
resets the input tag's value on each files
country alter.
Before nosotros write the HTML for the second part, keep in mind that React just allows for returning one parent element from a component. Thus, we volition enclose both parts in a <></>
tag.
Here is the component with the HTML for both parts:
import React, { useRef, useState } from "react"; const DEFAULT_MAX_FILE_SIZE_IN_BYTES = 500000; const KILO_BYTES_PER_BYTE = 1000; const convertBytesToKB = (bytes) => Math.round(bytes / KILO_BYTES_PER_BYTE); const FileUpload = ({ label, updateFilesCb, maxFileSizeInBytes = DEFAULT_MAX_FILE_SIZE_IN_BYTES, ...otherProps }) => { const fileInputField = useRef(null); const [files, setFiles] = useState({}); return ( <> <section> <label>{characterization}</label> <p>Elevate and drib your files anywhere or</p> <push blazon="button"> <i className="fas fa-file-upload" /> <bridge> Upload {otherProps.multiple ? "files" : "a file"}</span> </button> <input type="file" ref={fileInputField} title="" value="" {...otherProps} /> </section> {/*2nd role starts here*/} <article> <span>To Upload</bridge> <section> {Object.keys(files).map((fileName, index) => { let file = files[fileName]; let isImageFile = file.type.split("/")[0] === "image"; return ( <department key={fileName}> <div> {isImageFile && ( <img src={URL.createObjectURL(file)} alt={`file preview ${index}`} /> )} <div isImageFile={isImageFile}> <bridge>{file.name}</bridge> <aside> <span>{convertBytesToKB(file.size)} kb</span> <i className="fas fa-trash-alt" /> </aside> </div> </div> </section> ); })} </department> </article> </> ); }; export default FileUpload;
In the second part of the HTML, nosotros are iterating through each file in the files
state and displaying the file name, size in KB, and an image preview if the file type is image/*
(i.e. png, jpg...etc).
To display an image preview, we are using the URL.createObjectURL
function. The createObjectURL function takes an object, which in this instance is a File object, and returns a temporary URL for accessing the file. We can and so set that URL to src
attribute of an img tag.
Styling
Nosotros will now utilise the styled-components package we installed before.
Add the following in the file-upload.styles.js
file:
import styled from "styled-components"; export const FileUploadContainer = styled.section` position: relative; margin: 25px 0 15px; edge: 2px dotted lightgray; padding: 35px 20px; border-radius: 6px; display: flex; flex-management: cavalcade; align-items: center; background-colour: white; `; export const FormField = styled.input` font-size: 18px; display: block; width: 100%; edge: none; text-transform: none; position: accented; acme: 0; left: 0; correct: 0; bottom: 0; opacity: 0; &:focus { outline: none; } `; export const InputLabel = styled.label` top: -21px; font-size: 13px; color: black; left: 0; position: absolute; `; export const DragDropText = styled.p` font-weight: bold; letter-spacing: 2.2px; margin-superlative: 0; text-align: center; `; export const UploadFileBtn = styled.button` box-sizing: border-box; advent: none; groundwork-colour: transparent; border: 2px solid #3498db; cursor: pointer; font-size: 1rem; line-height: ane; padding: 1.1em 2.8em; text-align: center; text-transform: uppercase; font-weight: 700; edge-radius: 6px; color: #3498db; position: relative; overflow: hidden; z-alphabetize: 1; transition: color 250ms ease-in-out; font-family: "Open Sans", sans-serif; width: 45%; display: flex; marshal-items: center; padding-right: 0; justify-content: heart; &:after { content: ""; position: absolute; display: block; top: 0; left: l%; transform: translateX(-50%); width: 0; height: 100%; background: #3498db; z-alphabetize: -1; transition: width 250ms ease-in-out; } i { font-size: 22px; margin-correct: 5px; edge-right: 2px solid; position: absolute; top: 0; bottom: 0; left: 0; right: 0; width: 20%; display: flex; flex-direction: column; justify-content: center; } @media just screen and (max-width: 500px) { width: 70%; } @media simply screen and (max-width: 350px) { width: 100%; } &:hover { color: #fff; outline: 0; background: transparent; &:after { width: 110%; } } &:focus { outline: 0; background: transparent; } &:disabled { opacity: 0.four; filter: grayscale(100%); arrow-events: none; } `; export const FilePreviewContainer = styled.commodity` margin-bottom: 35px; span { font-size: 14px; } `; export const PreviewList = styled.section` display: flex; flex-wrap: wrap; margin-top: 10px; @media merely screen and (max-width: 400px) { flex-direction: column; } `; export const FileMetaData = styled.div` brandish: ${(props) => (props.isImageFile ? "none" : "flex")}; flex-direction: cavalcade; position: absolute; top: 0; left: 0; right: 0; bottom: 0; padding: 10px; edge-radius: 6px; color: white; font-weight: bold; background-colour: rgba(five, five, 5, 0.55); aside { margin-superlative: automobile; display: flex; justify-content: space-between; } `; consign const RemoveFileIcon = styled.i` cursor: pointer; &:hover { transform: scale(1.three); } `; export const PreviewContainer = styled.section` padding: 0.25rem; width: 20%; acme: 120px; border-radius: 6px; box-sizing: border-box; &:hover { opacity: 0.55; ${FileMetaData} { display: flex; } } & > div:start-of-type { height: 100%; position: relative; } @media only screen and (max-width: 750px) { width: 25%; } @media only screen and (max-width: 500px) { width: 50%; } @media only screen and (max-width: 400px) { width: 100%; padding: 0 0 0.4em; } `; consign const ImagePreview = styled.img` border-radius: 6px; width: 100%; height: 100%; `;
When using styled-components, we are creating components that render an HTML tag with some styles. For example, the ImagePreview
is a component that renders an img
tag with the styles within the tagged template literal.
Since nosotros are creating components, we can pass props to information technology and access it when writing the styles (e.thou. FileMetaData
in the example above).
We have at present finished the styling and adding drag and drop.
"But wait, when did we add together elevate and drib?"
Past default, the file input tag supports drag and driblet. We simply just styled the input tag and fabricated it absolutely positioned (refer to FormField
above).
To utilise the styles we wrote, import all the styled components and replace the HTML in the file-upload.component.jsx
file.
import React, { useRef, useState } from "react"; import { FileUploadContainer, FormField, DragDropText, UploadFileBtn, FilePreviewContainer, ImagePreview, PreviewContainer, PreviewList, FileMetaData, RemoveFileIcon, InputLabel } from "./file-upload.styles"; const DEFAULT_MAX_FILE_SIZE_IN_BYTES = 500000; const KILO_BYTES_PER_BYTE = grand; const convertBytesToKB = (bytes) => Math.round(bytes / KILO_BYTES_PER_BYTE); const FileUpload = ({ label, updateFilesCb, maxFileSizeInBytes = DEFAULT_MAX_FILE_SIZE_IN_BYTES, ...otherProps }) => { const fileInputField = useRef(nil); const [files, setFiles] = useState({}); return ( <> <FileUploadContainer> <InputLabel>{label}</InputLabel> <DragDropText>Drag and drop your files anywhere or</DragDropText> <UploadFileBtn blazon="button"> <i className="fas fa-file-upload" /> <span> Upload {otherProps.multiple ? "files" : "a file"}</span> </UploadFileBtn> <FormField type="file" ref={fileInputField} title="" value="" {...otherProps} /> </FileUploadContainer> <FilePreviewContainer> <span>To Upload</bridge> <PreviewList> {Object.keys(files).map((fileName, index) => { let file = files[fileName]; allow isImageFile = file.blazon.divide("/")[0] === "image"; return ( <PreviewContainer key={fileName}> <div> {isImageFile && ( <ImagePreview src={URL.createObjectURL(file)} alt={`file preview ${index}`} /> )} <FileMetaData isImageFile={isImageFile}> <span>{file.proper name}</bridge> <aside> <span>{convertBytesToKB(file.size)} kb</span> <RemoveFileIcon className="fas fa-trash-alt" /> </aside> </FileMetaData> </div> </PreviewContainer> ); })} </PreviewList> </FilePreviewContainer> </> ); } export default FileUpload;
Functionality
We are almost finished with the file-upload component, we just need to add functions then that files
land can be modified.
Earlier we created a DOM reference using the useRef hook. Nosotros volition now apply that to open the file explorer once the "Upload Files" push is clicked. To do this, add the following role within the component:
const handleUploadBtnClick = () => { fileInputField.current.click(); };
We also need to add together an onClick
attribute to the UploadFileBtn
component to trigger the function above.
<UploadFileBtn type="button" onClick={handleUploadBtnClick}>
To process the files selected by the user once the "Upload Files" button is clicked, we need to add an onChange
attribute to the FormField
component.
<FormField type="file" ref={fileInputField} onChange={handleNewFileUpload} title="" value="" {...otherProps} />
Like with any DOM event (e.g. onClick
), the function to handle the event will have access to the event object. And so, the handleNewFileUpload
part will have the effect object every bit its start parameter.
const handleNewFileUpload = (eastward) => { const { files: newFiles } = e.target; if (newFiles.length) { allow updatedFiles = addNewFiles(newFiles); setFiles(updatedFiles); callUpdateFilesCb(updatedFiles); } };
In the function in a higher place, we access the files selected past the user from the eastward.target.files
holding then pass it to a role called addNewFiles
. Then, we take the return value from addNewFiles
and pass information technology to the setFiles
to update the files
state. Since any changes to the files
state must exist sent to the parent component, nosotros need to call the callUpdateFilesCb
office.
The addNewFiles
function takes a FileList object (east.target.files
above returns a FileList), iterates through it, and returns an object where the key is the file proper name and the value is the File object.
const addNewFiles = (newFiles) => { for (let file of newFiles) { if (file.size <= maxFileSizeInBytes) { if (!otherProps.multiple) { return { file }; } files[file.proper name] = file; } } return { ...files }; };
"Why are checking if in that location is not a multiple
property in otherProps
?"
Equally explained earlier, we are using the otherProps
variable to add attributes to the file input tag. And then, if we don't laissez passer a multiple
prop to the file upload component, then the file input tag does not allow for selecting multiple files. Put but, if there is a multiple
prop, selected files will get added to the files
land. Otherwise, selecting a new file volition remove the previous files
state and replace it with the newly selected file.
The callUpdateFilesCb
office takes the value returned from addNewFiles
, converts the files
state to an array and calls the updateFilesCb
function (from the props).
"Why do we pass updatedFiles
to callUpdateFilesCb
when we could just use the files
state inside the function?"
Since React state updates are asynchronous, at that place is no guarantee that when the callUpdateFilesCb
gets called, the files
state will accept inverse.
"Why practice nosotros have to catechumen the files
land to an array?"
We don't! However, when uploading files in the files
state to some third political party service (e.g. Firebase Deject Storage), it's easier to piece of work with arrays.
const convertNestedObjectToArray = (nestedObj) => Object.keys(nestedObj).map((primal) => nestedObj[key]); const callUpdateFilesCb = (files) => { const filesAsArray = convertNestedObjectToArray(files); updateFilesCb(filesAsArray); };
To remove a file, nosotros first demand to add an onClick
attribute to the RemoveFileIcon
component.
<RemoveFileIcon className="fas fa-trash-alt" onClick={() => removeFile(fileName)} />
The removeFile
function will have a file name, delete it from the files
state, update the files
land, and inform the parent component of the changes.
const removeFile = (fileName) => { delete files[fileName]; setFiles({ ...files }); callUpdateFilesCb({ ...files }); };
Here is the component with all the functions above:
import React, { useRef, useState } from "react"; import { FileUploadContainer, FormField, DragDropText, UploadFileBtn, FilePreviewContainer, ImagePreview, PreviewContainer, PreviewList, FileMetaData, RemoveFileIcon, InputLabel } from "./file-upload.styles"; const KILO_BYTES_PER_BYTE = 1000; const DEFAULT_MAX_FILE_SIZE_IN_BYTES = 500000; const convertNestedObjectToArray = (nestedObj) => Object.keys(nestedObj).map((primal) => nestedObj[key]); const convertBytesToKB = (bytes) => Math.circular(bytes / KILO_BYTES_PER_BYTE); const FileUpload = ({ label, updateFilesCb, maxFileSizeInBytes = DEFAULT_MAX_FILE_SIZE_IN_BYTES, ...otherProps }) => { const fileInputField = useRef(cypher); const [files, setFiles] = useState({}); const handleUploadBtnClick = () => { fileInputField.current.click(); }; const addNewFiles = (newFiles) => { for (allow file of newFiles) { if (file.size < maxFileSizeInBytes) { if (!otherProps.multiple) { return { file }; } files[file.proper noun] = file; } } return { ...files }; }; const callUpdateFilesCb = (files) => { const filesAsArray = convertNestedObjectToArray(files); updateFilesCb(filesAsArray); }; const handleNewFileUpload = (due east) => { const { files: newFiles } = eastward.target; if (newFiles.length) { let updatedFiles = addNewFiles(newFiles); setFiles(updatedFiles); callUpdateFilesCb(updatedFiles); } }; const removeFile = (fileName) => { delete files[fileName]; setFiles({ ...files }); callUpdateFilesCb({ ...files }); }; return ( <> <FileUploadContainer> <InputLabel>{characterization}</InputLabel> <DragDropText>Drag and drop your files anywhere or</DragDropText> <UploadFileBtn type="push" onClick={handleUploadBtnClick}> <i className="fas fa-file-upload" /> <bridge> Upload {otherProps.multiple ? "files" : "a file"}</span> </UploadFileBtn> <FormField type="file" ref={fileInputField} onChange={handleNewFileUpload} title="" value="" {...otherProps} /> </FileUploadContainer> <FilePreviewContainer> <span>To Upload</span> <PreviewList> {Object.keys(files).map((fileName, index) => { let file = files[fileName]; let isImageFile = file.type.dissever("/")[0] === "image"; return ( <PreviewContainer key={fileName}> <div> {isImageFile && ( <ImagePreview src={URL.createObjectURL(file)} alt={`file preview ${index}`} /> )} <FileMetaData isImageFile={isImageFile}> <bridge>{file.name}</span> <aside> <span>{convertBytesToKB(file.size)} kb</span> <RemoveFileIcon className="fas fa-trash-alt" onClick={() => removeFile(fileName)} /> </aside> </FileMetaData> </div> </PreviewContainer> ); })} </PreviewList> </FilePreviewContainer> </> ); }; export default FileUpload;
Use Example
Let's now use the file upload component in App component to see it in action!
In the App.js
file, nosotros will create a simple form and add state to store the form data.
import React, { useState } from "react"; part App() { const [newUserInfo, setNewUserInfo] = useState({ profileImages: [] }); const handleSubmit = (event) => { upshot.preventDefault(); //logic to create a new user... }; render ( <div> <form onSubmit={handleSubmit}> <button type="submit">Create New User</push button> </form> </div> ); } export default App;
At present to add the file upload component.
import React, { useState } from "react"; import FileUpload from "./components/file-upload/file-upload.component"; function App() { const [newUserInfo, setNewUserInfo] = useState({ profileImages: [] }); const handleSubmit = (consequence) => { event.preventDefault(); //logic to create a new user... }; render ( <div> <grade onSubmit={handleSubmit}> <FileUpload accept=".jpg,.png,.jpeg" label="Contour Image(s)" multiple /> <button blazon="submit">Create New User</button> </form> </div> ); } export default App;
Notice nosotros have non added the updateFilesCb
prop yet. Before we tin do that, we need to create a function that updates only the profileImages
property of the newUserInfo
state.
const updateUploadedFiles = (files) => setNewUserInfo({ ...newUserInfo, profileImages: files });
We will now pass this office every bit the updateFilesCb
prop. And so, whatsoever time the files
state changes in the file upload component, information technology will be saved in the profileImages
holding of the newUserInfo
land.
import React, { useState } from "react"; import FileUpload from "./components/file-upload/file-upload.component"; office App() { const [newUserInfo, setNewUserInfo] = useState({ profileImages: [] }); const updateUploadedFiles = (files) => setNewUserInfo({ ...newUserInfo, profileImages: files }); const handleSubmit = (event) => { upshot.preventDefault(); //logic to create new user... }; return ( <div> <grade onSubmit={handleSubmit}> <FileUpload have=".jpg,.png,.jpeg" label="Profile Paradigm(s)" multiple updateFilesCb={updateUploadedFiles} /> <button type="submit">Create New User</push> </grade> </div> ); } export default App;
"Why are we passing the take
and multiple
prop to the file upload component?"
Since whatever boosted props volition go passed to the file input tag, the file input tag volition have an accept
and multiple
attribute.
The multiple
aspect allows a user to select multiple files in the file explorer.
The have
attribute prevents users from selecting file types different from the ones specified (i.e. jpg, png, jpeg in this case).
Now that we are finished, run npm starting time
and visit localhost:3000. The following should announced:
For reference, the lawmaking can exist establish at
https://github.com/Chandra-Panta-Chhetri/react-file-upload-tutorial.
Source: https://dev.to/chandrapantachhetri/responsive-react-file-upload-component-with-drag-and-drop-4ef8
0 Response to "Creating a React Component That Allows Csv and Txt File Upload"
Post a Comment