DEV Community

Cover image for Responsive React File Upload Component With Drag And Drop
Chandra Panta Chhetri
Chandra Panta Chhetri

Posted on • Edited on

Responsive React File Upload Component With Drag And Drop

While working on a React project, I implemented a responsive file upload component that supports drag and drop without using any libraries. Most of the file upload components online used libraries such as react-dropzone to support drag and drop. So, I thought I'd share how I made the component and show a typical use case for it.

End result

Figure 1: File upload component used in a form (example use case)

Figure 2: Responsive file upload component

The features include:

  • drag and drop without using any libraries
  • displaying image preview for image files
  • displaying file size & name
  • removing files in the "To Upload" section
  • preventing user from uploading files bigger than a specified size
    • Note: this should also be 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 project is by using create-react-app. So, run the following commands in a terminal/command-line:

npx create-react-app react-file-upload
cd react-file-upload
Enter fullscreen mode Exit fullscreen mode

To ensure everything was set up properly after you run npm start, the following should appear once you visit localhost:3000 in a browser:

Alt Text

Before building the component, let's modify and remove some files to get rid of unnecessary code.

  • Change App.js to the following:
import React from 'react';

function App() {
  return (
    <div></div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode
  • Change index.js to the following:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode

Remove all files in the src folder except

  • App.js
  • index.js
  • index.css

File Upload Component

Installing Dependencies

The dependencies we will need are:

styled-components

  • For styling the component
    • styled components allow for style 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 Structure

A good 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 then a file-upload folder within the components folder.

Lastly, within the file-upload folder, create 2 new files.

  • file-upload.component.jsx
  • file-upload.styles.js

State

Since we are creating a functional component and need to use state, we will use the useState hook.

The useState hook returns a stateful value which is the same as the value passed as the first argument, and a function to update it.

For our purposes, we will need state to keep track of the uploaded files. So, in the file-upload.component.jsx file, add the following:

import React, { useState } from "react";

const FileUpload = () => {
  const [files, setFiles] = useState({});

  return (
   <div></div>
  )
}

export default FileUpload;
Enter fullscreen mode Exit fullscreen mode

“Shouldn't we use an empty array instead of an empty object for the files state?”

Using an object will allow us to easily manipulate (add/remove) the files state and prevent files with the same name from being uploaded more than once. Here is an example of how the files state will look like:

{
 "file1.png": File,
 "file2.png": File
}
Enter fullscreen mode Exit fullscreen mode

If we used an array it would require 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 can be found at https://developer.mozilla.org/en-US/docs/Web/API/File.

useRef hook

If you look at Figure 1 above, you will 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. However, we want to open it once the Upload Files button is clicked so we will require a DOM reference to the file input tag.

To create a DOM reference, we will use the useRef hook. The useRef hook returns a mutable ref object whose .current property refers to a DOM node (file input tag in this case).

Once we use the useRef hook, we must pass the returned value to the ref attribute of the file input tag, like so:

import React, { useState, useRef } from "react";

const FileUpload = (props) => {
  const fileInputField = useRef(null);
  const [files, setFiles] = useState({});

  return (
   <input type="file" ref={fileInputField} />
  )
}

export default FileUpload;
Enter fullscreen mode Exit fullscreen mode

Props

The component will have the following props:

  • label
    • Determines the label of the component (e.g. "Profile Image(s)" in Figure 1 above)
  • maxFileSizeInBytes
    • For preventing files above the specified size from being uploaded
  • updateFilesCb
    • A callback function used for sending the files state to the parent component

“Why do we need to send the files state 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 need the file upload component to send it.

“Why do we need use a callback function to send the files state to the parent component?”

Since React has unidirectional data flow, we cannot easily pass data from the child component (file upload component) to the parent component. As a workaround, we will pass a function declared in the parent component and the file upload component will call that function with the files state as an argument. This process of sending data 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 the props like so:

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 type="file" ref={fileInputField} />
  )
}

export default FileUpload;
Enter fullscreen mode Exit fullscreen mode

“Why are we using the spread syntax when destructuring otherProps?”

When destructuring, we can assign all other values that were not explicitly destructured to a variable.

let props = { a: 1, b: 2, c: 3};
let {a, ...otherProps} = props;

//a = 1
//otherProps = {b: 2, c: 3};
Enter fullscreen mode Exit fullscreen mode

In this case, for any props that we do not destructure, they will be assigned to the otherProps variable. We will see the use of this otherProps variable later.

HTML

For the icons shown in Figure 1, we will be using Font Awesome. To import it, add the following in the head tag in the public/index.html file:

<link
 rel="stylesheet"
 href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css"
/>
Enter fullscreen mode Exit fullscreen mode

From Figure 1, it is evident we can split the HTML for the component into 2 main parts.

Main parts of component

Here is the component with the HTML for the first part:

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 (
      <section>
        <label>{label}</label>
        <p>Drag and drop your files anywhere or</p>
        <button type="button">
          <i className="fas fa-file-upload" />
          <span> Upload {otherProps.multiple ? "files" : "a file"}</span>
        </button>
        <input
          type="file"
          ref={fileInputField}
          title=""
          value=""
          {...otherProps}
        />
      </section>      
  );
}

export default FileUpload;
Enter fullscreen mode Exit fullscreen mode

Earlier, we discussed that any props that we don't destructure will be assigned to the otherProps variable (i.e. 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 done so that we can add attributes to the file input tag from the parent component via props.

“Why are we setting the title and value attribute to ""?”

Setting the title attribute to "" gets rid of the text that shows up by default when hovering over the input tag ("No file chosen").

Setting the value attribute to "" fixes an edge case where uploading a file right after removing it does not change the files state. Later, we will see that the files state only changes once the value of the input tag changes. This bug occurs because when we remove a file, the input tag's value does not change. Since state changes re-render HTML, setting the value attribute to "" resets the input tag's value on each files state change.

Before we write the HTML for the second part, keep in mind that React only allows for returning one parent element from a component. Thus, we will 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>{label}</label>
        <p>Drag and drop your files anywhere or</p>
        <button type="button">
          <i className="fas fa-file-upload" />
          <span> Upload {otherProps.multiple ? "files" : "a file"}</span>
        </button>
        <input
          type="file"
          ref={fileInputField}
          title=""
          value=""
          {...otherProps}
        />
      </section>

      {/*second part starts here*/}
      <article>
        <span>To Upload</span>
        <section>
          {Object.keys(files).map((fileName, index) => {
            let file = files[fileName];
            let isImageFile = file.type.split("/")[0] === "image";
            return (
              <section key={fileName}>
                <div>
                  {isImageFile && (
                    <img
                      src={URL.createObjectURL(file)}
                      alt={`file preview ${index}`}
                    />
                  )}
                  <div isImageFile={isImageFile}>
                    <span>{file.name}</span>
                    <aside>
                      <span>{convertBytesToKB(file.size)} kb</span>
                      <i className="fas fa-trash-alt" />
                    </aside>
                  </div>
                </div>
              </section>
            );
          })}
        </section>
      </article>
    </>
  );
};

export default FileUpload;
Enter fullscreen mode Exit fullscreen mode

In the second part of the HTML, we 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 case is a File object, and returns a temporary URL for accessing the file. We can then set that URL to src attribute of an img tag.

Styling

We will now use the styled-components package we installed earlier.

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;
  border: 2px dotted lightgray;
  padding: 35px 20px;
  border-radius: 6px;
  display: flex;
  flex-direction: column;
  align-items: center;
  background-color: white;
`;

export const FormField = styled.input`
  font-size: 18px;
  display: block;
  width: 100%;
  border: none;
  text-transform: none;
  position: absolute;
  top: 0;
  left: 0;
  right: 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-top: 0;
  text-align: center;
`;

export const UploadFileBtn = styled.button`
  box-sizing: border-box;
  appearance: none;
  background-color: transparent;
  border: 2px solid #3498db;
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  padding: 1.1em 2.8em;
  text-align: center;
  text-transform: uppercase;
  font-weight: 700;
  border-radius: 6px;
  color: #3498db;
  position: relative;
  overflow: hidden;
  z-index: 1;
  transition: color 250ms ease-in-out;
  font-family: "Open Sans", sans-serif;
  width: 45%;
  display: flex;
  align-items: center;
  padding-right: 0;
  justify-content: center;

  &:after {
    content: "";
    position: absolute;
    display: block;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 0;
    height: 100%;
    background: #3498db;
    z-index: -1;
    transition: width 250ms ease-in-out;
  }

  i {
    font-size: 22px;
    margin-right: 5px;
    border-right: 2px solid;
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    width: 20%;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  @media only screen and (max-width: 500px) {
    width: 70%;
  }

  @media only 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.4;
    filter: grayscale(100%);
    pointer-events: none;
  }
`;

export const FilePreviewContainer = styled.article`
  margin-bottom: 35px;

  span {
    font-size: 14px;
  }
`;

export const PreviewList = styled.section`
  display: flex;
  flex-wrap: wrap;
  margin-top: 10px;

  @media only screen and (max-width: 400px) {
    flex-direction: column;
  }
`;

export const FileMetaData = styled.div`
  display: ${(props) => (props.isImageFile ? "none" : "flex")};
  flex-direction: column;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 10px;
  border-radius: 6px;
  color: white;
  font-weight: bold;
  background-color: rgba(5, 5, 5, 0.55);

  aside {
    margin-top: auto;
    display: flex;
    justify-content: space-between;
  }
`;

export const RemoveFileIcon = styled.i`
  cursor: pointer;

  &:hover {
    transform: scale(1.3);
  }
`;

export const PreviewContainer = styled.section`
  padding: 0.25rem;
  width: 20%;
  height: 120px;
  border-radius: 6px;
  box-sizing: border-box;

  &:hover {
    opacity: 0.55;

    ${FileMetaData} {
      display: flex;
    }
  }

  & > div:first-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;
  }
`;

export const ImagePreview = styled.img`
  border-radius: 6px;
  width: 100%;
  height: 100%;
`;
Enter fullscreen mode Exit fullscreen mode

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 inside the tagged template literal.

Since we are creating components, we can pass props to it and access it when writing the styles (e.g. FileMetaData in the example above).

We have now finished the styling and adding drag and drop.

“But wait, when did we add drag and drop?”

By default, the file input tag supports drag and drop. We simply just styled the input tag and made it absolutely positioned (refer to FormField above).

To use 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 = 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 (
    <>
      <FileUploadContainer>
        <InputLabel>{label}</InputLabel>
        <DragDropText>Drag and drop your files anywhere or</DragDropText>
        <UploadFileBtn type="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</span>
        <PreviewList>
          {Object.keys(files).map((fileName, index) => {
            let file = files[fileName];
            let isImageFile = file.type.split("/")[0] === "image";
            return (
              <PreviewContainer key={fileName}>
                <div>
                  {isImageFile && (
                    <ImagePreview
                      src={URL.createObjectURL(file)}
                      alt={`file preview ${index}`}
                    />
                  )}
                  <FileMetaData isImageFile={isImageFile}>
                    <span>{file.name}</span>
                    <aside>
                      <span>{convertBytesToKB(file.size)} kb</span>
                      <RemoveFileIcon
                        className="fas fa-trash-alt"
                      />
                    </aside>
                  </FileMetaData>
                </div>
              </PreviewContainer>
            );
          })}
        </PreviewList>
      </FilePreviewContainer>
    </>
  );
}

export default FileUpload;
Enter fullscreen mode Exit fullscreen mode

Functionality

We are almost finished with the file-upload component, we just need to add functions so that files state can be modified.

Earlier we created a DOM reference using the useRef hook. We will now use that to open the file explorer once the "Upload Files" button is clicked. To do this, add the following function within the component:

const handleUploadBtnClick = () => {
  fileInputField.current.click();
};
Enter fullscreen mode Exit fullscreen mode

We also need to add an onClick attribute to the UploadFileBtn component to trigger the function above.

<UploadFileBtn type="button" onClick={handleUploadBtnClick}>
Enter fullscreen mode Exit fullscreen mode

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}
/>
Enter fullscreen mode Exit fullscreen mode

Like with any DOM event (e.g. onClick), the function to handle the event will have access to the event object. So, the handleNewFileUpload function will have the event object as its first parameter.

 const handleNewFileUpload = (e) => {
    const { files: newFiles } = e.target;
    if (newFiles.length) {
      let updatedFiles = addNewFiles(newFiles);
      setFiles(updatedFiles);
      callUpdateFilesCb(updatedFiles);
    }
  };
Enter fullscreen mode Exit fullscreen mode

In the function above, we access the files selected by the user from the e.target.files property then pass it to a function called addNewFiles. Then, we take the return value from addNewFiles and pass it to the setFiles to update the files state. Since any changes to the files state must be sent to the parent component, we need to call the callUpdateFilesCb function.

The addNewFiles function takes a FileList object (e.target.files above returns a FileList), iterates through it, and returns an object where the key is the file 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.name] = file;
      }
    }
    return { ...files };
  };
Enter fullscreen mode Exit fullscreen mode

“Why are checking if there is not a multiple property in otherProps?”

As explained earlier, we are using the otherProps variable to add attributes to the file input tag. So, if we don't pass a multiple prop to the file upload component, then the file input tag does not allow for selecting multiple files. Put simply, if there is a multiple prop, selected files will get added to the files state. Otherwise, selecting a new file will remove the previous files state and replace it with the newly selected file.

The callUpdateFilesCb function 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 within the function?”

Since React state updates are asynchronous, there is no guarantee that when the callUpdateFilesCb gets called, the files state will have changed.

"Why do we have to convert the files state to an array?"

We don't! However, when uploading files in the files state to some third party service (e.g. Firebase Cloud Storage), it's easier to work with arrays.

const convertNestedObjectToArray = (nestedObj) =>
  Object.keys(nestedObj).map((key) => nestedObj[key]);

const callUpdateFilesCb = (files) => {
  const filesAsArray = convertNestedObjectToArray(files);
  updateFilesCb(filesAsArray);
};
Enter fullscreen mode Exit fullscreen mode

To remove a file, we first need to add an onClick attribute to the RemoveFileIcon component.

<RemoveFileIcon
  className="fas fa-trash-alt"
  onClick={() => removeFile(fileName)}
/>
Enter fullscreen mode Exit fullscreen mode

The removeFile function will take a file name, delete it from the files state, update the files state, and inform the parent component of the changes.

const removeFile = (fileName) => {
  delete files[fileName];
  setFiles({ ...files });
  callUpdateFilesCb({ ...files });
};
Enter fullscreen mode Exit fullscreen mode

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((key) => nestedObj[key]);

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({});

  const handleUploadBtnClick = () => {
    fileInputField.current.click();
  };

  const addNewFiles = (newFiles) => {
    for (let file of newFiles) {
      if (file.size < maxFileSizeInBytes) {
        if (!otherProps.multiple) {
          return { file };
        }
        files[file.name] = file;
      }
    }
    return { ...files };
  };

  const callUpdateFilesCb = (files) => {
    const filesAsArray = convertNestedObjectToArray(files);
    updateFilesCb(filesAsArray);
  };

  const handleNewFileUpload = (e) => {
    const { files: newFiles } = e.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>{label}</InputLabel>
        <DragDropText>Drag and drop your files anywhere or</DragDropText>
        <UploadFileBtn type="button" onClick={handleUploadBtnClick}>
          <i className="fas fa-file-upload" />
          <span> 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.split("/")[0] === "image";
            return (
              <PreviewContainer key={fileName}>
                <div>
                  {isImageFile && (
                    <ImagePreview
                      src={URL.createObjectURL(file)}
                      alt={`file preview ${index}`}
                    />
                  )}
                  <FileMetaData isImageFile={isImageFile}>
                    <span>{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;
Enter fullscreen mode Exit fullscreen mode

Use Case

Let's now use the file upload component in App component to see it in action!

In the App.js file, we will create a simple form and add state to store the form data.

import React, { useState } from "react";

function App() {
  const [newUserInfo, setNewUserInfo] = useState({
    profileImages: []
  });

  const handleSubmit = (event) => {
    event.preventDefault();
    //logic to create a new user...
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <button type="submit">Create New User</button>
      </form>
    </div>
  );
}
export default App;
Enter fullscreen mode Exit fullscreen mode

Now 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 = (event) => {
    event.preventDefault();
    //logic to create a new user...
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <FileUpload
          accept=".jpg,.png,.jpeg"
          label="Profile Image(s)"
          multiple
        />
        <button type="submit">Create New User</button>
      </form>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Notice we have not added the updateFilesCb prop yet. Before we can 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 });
Enter fullscreen mode Exit fullscreen mode

We will now pass this function as the updateFilesCb prop. So, any time the files state changes in the file upload component, it will be saved in the profileImages property of the newUserInfo state.

import React, { useState } from "react";
import FileUpload from "./components/file-upload/file-upload.component";

function App() {
  const [newUserInfo, setNewUserInfo] = useState({
    profileImages: []
  });

  const updateUploadedFiles = (files) =>
    setNewUserInfo({ ...newUserInfo, profileImages: files });

  const handleSubmit = (event) => {
    event.preventDefault();
    //logic to create new user...
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <FileUpload
          accept=".jpg,.png,.jpeg"
          label="Profile Image(s)"
          multiple
          updateFilesCb={updateUploadedFiles}
        />
        <button type="submit">Create New User</button>
      </form>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

“Why are we passing the accept and multiple prop to the file upload component?”

Since any additional props will get passed to the file input tag, the file input tag will have an accept and multiple attribute.

The multiple attribute allows a user to select multiple files in the file explorer.

The accept 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 start and visit localhost:3000. The following should appear:

Using the file upload component in a form

For reference, the code can be found at
https://github.com/Chandra-Panta-Chhetri/react-file-upload-tutorial.

Top comments (7)

Collapse
 
fatfire profile image
Dingyuan Gao • Edited

Is it a good way to change state directly?
like the code below

code image

Collapse
 
derick1530 profile image
Derick Zihalirwa

I don't think so

Collapse
 
akshaymittal143 profile image
Akshay Mittal

great post, thanks for sharing!

Collapse
 
andresezequiel profile image
andresezequielmanzanares

Hi, i am trying to implement this on chrome browser on on Iphone but I am unable to load the images in the preview. Is this tested on mobile ??

Great job BTW.

Collapse
 
derick1530 profile image
Derick Zihalirwa

Yes it is. don't forget that iphone has a .heif extension and the file size is bigger than the default set on this tutorial. all you need to do is to add the .heif and increase the default size in your code.

Hope this helps

Collapse
 
elagasamel profile image
Elagas Amel

merci

Collapse
 
hninwaiaung profile image
HninWaiAung

Is there any tutorial for upload to S3 and dynamoDB?