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
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
To ensure everything was set up properly after you run npm start
, the following should appear once you visit localhost:3000
in a browser:
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;
- 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')
);
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:
- For styling the component
- styled components allow for style encapsulation and creating dynamic styles via props
- 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;
“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
}
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;
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
- A callback function used for sending the
“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;
“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};
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"
/>
From Figure 1, it is evident we can split the HTML for the component into 2 main parts.
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;
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;
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%;
`;
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;
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();
};
We also need to add 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. 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);
}
};
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 };
};
“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);
};
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)}
/>
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 });
};
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;
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;
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;
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 });
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;
“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:
For reference, the code can be found at
https://github.com/Chandra-Panta-Chhetri/react-file-upload-tutorial.
Top comments (7)
Is it a good way to change state directly?
like the code below
I don't think so
great post, thanks for sharing!
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.
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
merci
Is there any tutorial for upload to S3 and dynamoDB?