Build A React Drag & Drop Progress File Uploader
In-Depth Walkthrough On How To Make A Drag & Drop File Uploader That Shows Its Upload Progress In React
What Are We Building
If you have ever seen a progress file uploader that exists online, like Google Drive, or other major web application, that is essential what we’re building today, but with a simpler UI, and in React.
Why Do This?
One of the main reasons to build this is to give users more context in terms of the state they are currently in, their progress, and give them more options while they are uploading files.
What I mean by this is that when you’re building a Minimal Viable Product web application, typically what happens is that the user is given a choice to just upload a file. Once the file is uploading, they are usually presented with a loader (like below), and have to wait until that file is done uploading to proceed with the next step. What we want to do is take this further and give the user a progress update, and even allow them to abort the process all-together.
Requirements
Before we start, the only thing that you need is the following to make sure you can get through everything.
- NodeJS 10.16.1
- Browser That Support FileAPI with FileReader
Step 1 — Backend API File Uploader
Before we can even create our frontend, we need to have a backend that accepts file uploads, for this we’ll make a simple Backend API that accepts files with a POST endpoint in NodeJS.
Create New NodeJS Project
# create new folder
mkdir uploadapi;
# enter folder
cd uploadd api;
# create new file
touch index.js;# create files directory
mkdir files;
# init
yarn init -y; # or npm init -y;
Install Dependencies
We’re going to use express
for the endpoints and multiparty
for handling the form data.
yarn add express; # npm install express;
yarn add multiparty; # npm install multiparty; # to handle form data
yarn add cors; # npm install cors; # allow requests from other ports
Create Application
Let’s create our NodeJS application:
File: /uploadapi/index.js
// Imports
const express = require('express');
const multiparty = require('multiparty');
const app = express();
const port = 5000;
const cors = require('cors');
const fs = require('fs');
const folder = 'files/';// CORS configurations
app.use(cors());// Endpoints
app.post('/upload', (req, res) => {
// initiate multiparty
const form = new multiparty.Form();
// parse req form data
return form.parse(req, (err, fields, files) => {
// error handling
if (err) {
return res.status(400).send({error: err });
}
// path
const { path } = files.file[0];
// get the temp file name from the tmp folder
let filename = path.split('/');
filename = filename[filename.length - 1];
// move file into folder
return fs.rename(path, `${folder}${filename}`, error => {
// error handling for moving
if (error) {
return res.status(400).send({ error });
}
return res.status(200).send({ file: filename });
});
});
});// Listen
app.listen(port, () => console.log(`Listening on port ${port}`));
Test File Upload With Postman
Now that we have our file, let’s start our server up and test a file upload with Postman.
Start our server up with:
node index.js# Expected output
# Listening on port 5000
Open up Postman, and make sure the settings are set as follows:
POST http://localhost:5000/uploadBody: form-data
key: file
value: youfile.png
Step 2—Create React App
Now that we have our backend setup, we need to scaffold out our React frontend with create-react-app
:
Create A New Project Folder
# create new directory
mkdir react-uploader;# scaffold out create-react-app
npx create-react-app react-uploader;
Remove Unnecessary Files
# Remove files
rm react-uploader/src/App.css;
rm react-uploader/src/App.js;
rm react-uploader/src/App.test.js;
rm react-uploader/src/logo.svg;
rm react-uploader/src/index.css;# Create blank file
touch react-uploader/src/App.js;
touch react-uploader/src/index.css;
Step 3— Creating Our UI
The way we’re going to create our UI is that we’re going to have a drop area that will accept a file, show a preview (if it’s an image), and have a progress percentage display in the middle.
If we break this up into components, we should have something like this:
- App
- ImagePreview
- DropArea
- ImageStatus
- Status
Creating Our Main Component
Our main structure for our App.js
should be pretty simple:
File: /react-uploader/src/App.js
import React from 'react';const App = () => {
return (
<div className="App">
</div>
);
};export default App;
We might just want to modify our index.css
file to adjust for the default browser margins and padding:
File: /react-uploader/src/index.css
html, body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
}
We also wanted to make sure our child div for the App
is centered with Flexbox
:
File: /react-uploader/src/index.css
html, body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
}.App {
display: flex;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
}
Create DropArea & Status Component
Next let’s create our drop area and make it slightly smaller than main App
and add some transparent borders to it to start, so that we can account for their width when we add a hover effect to make the border a color.
File: /react-uploader/src/App.js
import React from 'react';const App = () => {
return (
<div className="App">
<div className="DropArea">
<div className="Status">Drop Here</div>
</div>
</div>
);
};export default App;
File: /react-uploader/src/index.css
html, body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
}.App {
display: flex;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
}.DropArea {
background: #efefef;
display: flex;
align-items: center;
justify-content: center;
width: calc(80vw - 80px);
height: calc(80vh - 80px);
border: solid 40px transparent;
transition: all 250ms ease-in-out 0s;
position: relative;
}.Status {
background: transparent;
display: block;
font-family: 'Helvetica', Arial, sans-serif;
color: black;
font-size: 60px;
font-weight: bold;
text-align: center;
line-height: calc(80vh - 80px);
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
transition: all 250ms ease-in-out 0s;
}
If we take a look at our project so far it should look like below:
ImageStatus Component
Next, what I want to do here is place an image in the middle to show the image we’re currently uploading as a preview. We’re going to use this image from Unsplash, well, because I love Montreal. We’ll place it in the same directory as our react project.
In our App.js
we’ll create our new div with a sub div to hold the image.
File: /react-uploader/src/App.js
import React from 'react';
import BgImage from './eva-blue-unsplash.jpg';const App = () => {
return (
<div className="App">
<div className="ImagePreview">
<div style={{ backgroundImage: `url(${BgImage})` }} />
</div>
<div className="DropArea">
<div className="Status">Drop Here</div>
</div>
</div>
);
};export default App;
Next, we’ll modify the css to make our background image cover the entire background and give it a blurred look:
File: /react-uploader/src/index.css
....App {
background-size: cover;
display: flex;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
}.ImagePreview {
display: block;
left: 0;
right: 0;
bottom: 0;
top: 0;
position: absolute;
overflow: hidden;
}.ImagePreview > div {
position: absolute;
background-size: cover;
filter: blur(20px);
left: -40px;
right: -40px;
bottom: -40px;
top: -40px;
}...
We should get the following:
I also want to be able to show a progress of the image being uploaded with its physical size, but using a mask. To do this, we’ll use two image placed over top of one another.
File: /react-uploader/src/App.js
import React from 'react';
import BgImage from './eva-blue-unsplash.jpg';const App = () => {
return (
<div className="App">
<div className="ImagePreview">
<div style={{ backgroundImage: `url(${BgImage})` }}></div>
</div>
<div className="DropArea">
<div className="ImageProgress">
<div className="ImageProgressImage" style={{ backgroundImage: `url(${BgImage})` }}></div>
<div className="ImageProgressUploaded" style={{ backgroundImage: `url(${BgImage})` }}></div>
</div>
<div className="Status">Drop Here</div>
</div>
</div>
);
};export default App;
In the css file, we’ll simulate the progress using clip-path
.
File: /react-uploader/src/index.css
....DropArea {
background: #efefef;
display: flex;
align-items: center;
justify-content: center;
width: calc(80vw - 80px);
height: calc(80vh - 80px);
border: solid 40px transparent;
transition: all 250ms ease-in-out 0s;
position: relative;
}.ImageProgress {
display: block;
left: 0;
right: 0;
top: 0;
bottom: 0;
position: absolute;
overflow: hidden;
}.ImageProgress > .ImageProgressImage {
opacity: 0.3;
position: absolute;
background-position: center center;
background-size: cover;
top: 0;
left: 0;
right: 0;
bottom: 0;
}.ImageProgress > .ImageProgressUploaded {
position: absolute;
background-position: center center;
background-size: cover;
top: 0;
left: 0;
right: 0;
bottom: 0;
clip-path: inset(50% 0 0 0);
}...
If we save our work, we should something like the following:
Let’s make a slight adjustment to make the “Drop Here” text more visible with the image. We’ll give it a background with a semi-transparent black and white text instead.
File: /react-uploader/src/index.js
....Status {
background: rgba(0, 0, 0, 0.3);
display: block;
font-family: 'Helvetica', Arial, sans-serif;
color: white;
font-size: 60px;
font-weight: bold;
text-align: center;
line-height: calc(80vh - 80px);
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
transition: all 250ms ease-in-out 0s;
}
Step 4— File Preview
Now we have all our UI elements created, it’s time to add the functionality of things. We need to create event handlers for dragging and dropping a file, handling only images, and then reading that file with FileReader.
Temporary Clean Up
We first need to get our divs in the original state, so we’re just going to comment them out in the meantime.
File: /react-uploader/src/App.js
import React from 'react';const App = () => {
return (
<div className="App">
{/* <div className="ImagePreview">
<div style={{ backgroundImage: `url(${BgImage})` }}> </div>
</div> */}
<div className="DropArea">
{/* <div className="ImageProgress">
<div className="ImageProgressImage" style={{ backgroundImage: `url(${BgImage})` }}></div>
<div className="ImageProgressUploaded" style={{ backgroundImage: `url(${BgImage})` }}></div>
</div> */}
<div className="Status">Drop Here</div>
</div>
</div>
);
};export default App;
We also want to reset our status background and color:
File: /react-uploader/src/index.css
.Status {
background: transparent;
display: block;
font-family: 'Helvetica', Arial, sans-serif;
color: black;
font-size: 60px;
font-weight: bold;
text-align: center;
line-height: calc(80vh - 80px);
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
transition: all 250ms ease-in-out 0s;
}
Creating Drag & Drop Handlers
Next, we’ll create event handlers on the main div className="App"
so that it can know when a file has been dragged to the window.
File: /react-uploader/src/App.js
...const App = () => {
const onDragEnter = event => {
console.log(event);
event.preventDefault();
}return (
<div className="App" onDragEnter={onDragEnter}>...
Now we if drag a file to our window (REMEMBER NOT TO DROP IT), you’ll see the event fired:
Next, we’ll use useState
to manage the status message to give the user feedback as when a file has been detected.
File: /react-uploader/src/App.js
import React, { useState } from 'react';
import BgImage from './eva-blue-unsplash.jpg';const App = () => {
const [status, setStatus] = useState('Drop Here'); const onDragEnter = event => {
console.log(event);
setStatus('File Detected');
event.preventDefault();
}...
<div className="Status">{status}</div>
</div>
</div>
);
};export default App;
We also need to handle with the file dragging has left the window:
File: /react-uploader/src/App.js
... const onDragEnter = event => {
setStatus('File Detected');
event.preventDefault();
} const onDragLeave = event => {
setStatus('Drop Here');
event.preventDefault();
}return (
<div className="App" onDragEnter={onDragEnter} onDragLeave={onDragLeave}>...
Now we’ll add a drag over event, with a twist to the css animation, and a drop event to start the process of reading the file.
File: /react-uploader/src/App.js
... const onDragEnter = event => {
setStatus('File Detected');
event.preventDefault();
event.stopPropagation();
} const onDragLeave = event => {
setStatus('Drop Here');
event.preventDefault();
} const onDragOver = event => {
setStatus('Drop');
event.preventDefault();
} const onDrop = event => {
console.log(event);
event.preventDefault();
}...<div className={`DropArea ${status === 'Drop' ? 'Over' : ''}`} onDragOver={onDragOver} onDrop={onDrop} onDragLeave={onDragEnter}>
and modify our css:
File: /react-uploader/src/index.css
....DropArea {
background: #efefef;
display: flex;
align-items: center;
justify-content: center;
width: calc(80vw - 80px);
height: calc(80vh - 80px);
border: solid 40px transparent;
transition: all 250ms ease-in-out 0s;
position: relative;
}.DropArea.Over {
border: solid 40px rgba(0, 0, 0, 0.2);
}...
Now while dragging we’ll see the following states:
Fixing onDrop Event For App Component
You’ll notice that if we drop the file too soon on the App
area it will load the image. We’ll fix this with a event.preventDefault()
.
File: /react-uploader/src/App.js
...const App = () => {
const [status, setStatus] = useState('Drop Here'); const doNothing = event => event.preventDefault();...
<div className="App" onDragEnter={onDragEnter} onDragLeave={onDragLeave} onDragOver={doNothing} onDrop={onDragLeave}>
Not when we drop the file on the className="App"
area, nothing happens and it resets as it should.
Detecting Supported File
We can now read the file when the file is dropped in the DropArea
by reading the event
.
File: /react-uploader/src/App.js
const onDrop = event => {
const supportedFilesTypes = ['image/jpeg', 'image/png'];
const { type } = event.dataTransfer.files[0];
if (supportedFilesTypes.indexOf(type) > -1) {
// continue with code
}
event.preventDefault();
};
Reading File
Now that we can confirm that a file is an image, we can read the file and set it for a new state called preview
.
File: /react-uploader/src/App.js
...const App = () => {
const [status, setStatus] = useState('Drop Here');
const [preview, setPreview] = useState(null);...const onDrop = event => {
const supportedFilesTypes = ['image/jpeg', 'image/png'];
const { type } = event.dataTransfer.files[0];
if (supportedFilesTypes.indexOf(type) > -1) {
// Begin Reading File
const reader = new FileReader();
reader.onload = e => setPreview(e.target.result);
reader.readAsDataURL(event.dataTransfer.files[0]);
}
event.preventDefault();
};...
Displaying File
To display the image that we just dropped, we’ll use the preview
state in our component.
File: /react-uploader/src/App.js
...<div className="App" onDragEnter={onDragEnter} onDragLeave={onDragLeave} onDragOver={doNothing} onDrop={onDragLeave}>
<div className={`ImagePreview ${preview ? 'Show' : ''}`}>
<div style={{ backgroundImage: `url(${preview})` }}></div>
</div>
<div className={`DropArea ${status === 'Drop' ? 'Over' : ''}`} onDragOver={onDragOver} onDragLeave={onDragEnter} onDrop={onDrop}>
<div className={`ImageProgress ${preview ? 'Show' : ''}`}>
<div className="ImageProgressImage" style={{ backgroundImage: `url(${preview})` }}></div>
<div className="ImageProgressUploaded" style={{ backgroundImage: `url(${preview})` }}></div>
</div>
<div className="Status">{status}</div>
</div>
</div>
...
To add a bit of smoother animation, we’ll modify our css file to add a transition
.
File: /react-uploader/src/index.css
...
.ImagePreview {
opacity: 0;
display: block;
left: 0;
right: 0;
bottom: 0;
top: 0;
position: absolute;
overflow: hidden;
transition: all 500ms ease-in-out 250ms;
}.ImagePreview.Show {
opacity: 1;
}....ImageProgress {
opacity: 0;
display: block;
left: 0;
right: 0;
top: 0;
bottom: 0;
position: absolute;
overflow: hidden;
transition: all 500ms ease-in-out 250ms;
}.ImageProgress.Show {
opacity: 1;
}...
When you load it, you should get that smooth fade-in with the image.
Step 5— Uploading File
Now that we can read the file, we can now also upload the file to our backend server with a progress update. For this though we’re doing to use an XMLHttpRequest
.
Why Not Fetch?
Unfortunately Fetch
does not have an event handle for upload progress, that’s why we’ll be using XHR
instead, which has more event handlers, aka more options.
Uploading The File
To start we’re just going to upload the file normally to make sure that it can communicate with our backend.
File: /react-uploader/src/App.js
...
const onDrop = event => {
const supportedFilesTypes = ['image/jpeg', 'image/png'];
const { type } = event.dataTransfer.files[0];
if (supportedFilesTypes.indexOf(type) > -1) {
// Begin Reading File
const reader = new FileReader();
reader.onload = e => setPreview(e.target.result);
reader.readAsDataURL(event.dataTransfer.files[0]); // Create Form Data
const payload = new FormData();
payload.append('file', event.dataTransfer.files[0]); // XHR - New XHR Request
const xhr = new XMLHttpRequest(); // XHR - Make Request
xhr.open('POST', 'http://localhost:5000/upload');
xhr.send(payload);
}
event.preventDefault();
};...
Now if you drag and drop an image to the drop area, you’ll see that the file gets successfully sent to your backend.
Step 6— XHR Events
Next we’re going to take advantage of an event that gets fired as our file gets progressively uploaded. This is with .upload.onprogress
. We’ll take advantage of this to both display the percentage as well as display the percentage in visual way.
File: /react-uploader/src/App.js
...const App = () => {
const [status, setStatus] = useState('Drop Here');
const [percentage, setPercentage] = useState(0);... const onDrop = event => {
const supportedFilesTypes = ['image/jpeg', 'image/png'];
const { type } = event.dataTransfer.files[0];
if (supportedFilesTypes.indexOf(type) > -1) {
// Begin Reading File
const reader = new FileReader();
reader.onload = e => setPreview(e.target.result);
reader.readAsDataURL(event.dataTransfer.files[0]); // Create Form Data
const payload = new FormData();
payload.append('file', event.dataTransfer.files[0]); // XHR - New XHR request
const xhr = new XMLHttpRequest(); // XHR - Upload Progress
xhr.upload.onprogress = (e) => {
const done = e.position || e.loaded
const total = e.totalSize || e.total;
const perc = (Math.floor(done/total*1000)/10);
if (perc >= 100) {
setStatus('Done');
} else {
setStatus(`${perc}%`);
}
setPercentage(perc);
}; // XHR - Make Request
xhr.open('POST', 'http://localhost:5000/upload');
xhr.send(payload);
}
event.preventDefault();
};...
We’re also going to modify our upload progress image to reflect this new percentage state.
File: /react-uploader/src/App.js
...
<div className={`ImageProgress ${preview ? 'Show' : ''}`}>
<div className="ImageProgressImage" style={{ backgroundImage: `url(${preview})` }}></div>
<div className="ImageProgressUploaded" style={{ backgroundImage: `url(${preview})`, clipPath: `inset(${100 - Number(percentage)}% 0 0 0);` }}></div>
</div>
...
and modify that css from our stylesheet to set an initial state and add a transition for smoother movement:
File: /react-uploader/src/index.css
....ImageProgress > .ImageProgressUploaded {
position: absolute;
background-position: center center;
background-size: cover;
top: 0;
left: 0;
right: 0;
bottom: 0;
clip-path: inset(0% 0 0 0);
transition: all 250ms ease-in-out 0ms;
}...
If you try and drag and drop a file, the upload my happen really quickly because the server connect is to your computer. So change this, we’re going to adjust the throttle to Fast 3G
speed in our Developer Tools:
Now when we drop our file, we should see the following percentages.
And there you have it. You have a drag and drop progress file uploader. The next few steps are more aesthetic cleanup and better state handling, but if you got value from this please skip to step Going From Here, otherwise continue with Step 7 — Cleaning Up.
Step 7 — Cleaning Up
The next steps are purely aesthetics and better handling so that we don’t bombard our browser with multiple files at once.
Prevent Further Drag & Drop While Uploading
For this we’re going to create a new state called enableDragDrop
which we’ll wrap around our different event handlers. This will prevent the user from dropping another file midway to start another upload and wait until the first file is finished uploading.
File: /react-uploader/src/App.js
...
const App = () => {
const [status, setStatus] = useState('Drop Here');
const [percentage, setPercentage] = useState(0);
const [preview, setPreview] = useState(null);
const [enableDragDrop, setEnableDragDrop] = useState(true);...const onDragEnter = event => {
if (enableDragDrop) {
setStatus('File Detected');
}
event.stopPropagation();
event.preventDefault();
};...
const onDragLeave = event => {
if (enableDragDrop) {
setStatus('Drop Here');
}
event.preventDefault();
};...
const onDragOver = event => {
if (enableDragDrop) {
setStatus('Drop');
}
event.preventDefault();
};...
const onDrop = event => {
const supportedFilesTypes = ['image/jpeg', 'image/png'];
const { type } = event.dataTransfer.files[0];
if (supportedFilesTypes.indexOf(type) > -1 && enableDragDrop) {
...
// XHR - Upload Progress
xhr.upload.onprogress = (e) => {
const done = e.position || e.loaded
const total = e.totalSize || e.total;
const perc = (Math.floor(done/total*1000)/10);
if (perc >= 100) {
setStatus('Done');
setEnableDragDrop(true);
} else {
setStatus(`${perc}%`);
}
setPercentage(perc);
};
...
// XHR - Make Request
xhr.open('POST', 'http://localhost:5000/upload');
xhr.send(payload); setEnableDragDrop(false);
}
event.preventDefault();
};
Reset When Done Uploading
File: /react-uploader/src/App.js
...
// XHR - Upload Progress
xhr.upload.onprogress = (e) => {
const done = e.position || e.loaded
const total = e.totalSize || e.total;
const perc = (Math.floor(done/total*1000)/10);
if (perc >= 100) {
setStatus('Done'); // Delayed reset
setTimeout(() => {
setPreview(null);
setStatus('Drop Here');
setPercentage(0);
setEnableDragDrop(true);
}, 750); // To match the transition 500 / 250
} else {
setStatus(`${perc}%`);
}
setPercentage(perc);
};
...
Modify Status For Better Visibility
I want to make sure our percentage and our status, while uploading is more visible.
File: /react-uploader/src/App.js
...<div className={`Status ${status.indexOf('%') > -1 || status === 'Done' ? 'Uploading' : ''}`}>{status}</div>...
File: /react-uploader/src/index.css
....Status {
background: transparent;
display: block;
font-family: 'Helvetica', Arial, sans-serif;
color: black;
font-size: 60px;
font-weight: bold;
text-align: center;
line-height: calc(80vh - 80px);
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
transition: all 250ms ease-in-out 0s;
}.Status.Uploading {
background: rgba(0, 0, 0, 0.3);
color: white;
}...
Hiding Our White Borders
Last thing I want to do is hide our white borders when the upload process has happened.
File: /react-uploader/src/App.js
...<div className={`DropArea ${status === 'Drop' ? 'Over' : ''} ${status.indexOf('%') > -1 || status === 'Done' ? 'Uploading' : ''}`} onDragOver={onDragOver} onDragLeave={onDragEnter} onDrop={onDrop}>
<div className={`ImageProgress ${preview ? 'Show' : ''}`}>...
File: /react-uploader/src/index.css
....DropArea.Over {
border: solid 40px rgba(0, 0, 0, 0.2);
}.DropArea.Uploading {
border-width: 0px;
}
...
Adding Upload Abort Button
One thing I want to make sure we have is a way to abort the upload process all-together and make sure we give user’s more options / actions when uploading files.
File: /react-uploader/src/App.js
...const [enableDragDrop, setEnableDragDrop] = useState(true);
const [stateXhr, setStateXhr] = useState(null);... // XHR - Make Request
xhr.open('POST', 'http://localhost:5000/upload');
xhr.send(payload); setStateXhr(xhr);
setEnableDragDrop(false);
}...
const onAbortClick = () => {
stateXhr.abort();
setPreview(null);
setStatus('Drop Here');
setPercentage(0);
setEnableDragDrop(true);
};...
<div className={`Status ${status.indexOf('%') > -1 || status === 'Done' ? 'Uploading' : ''}`}>{status}</div>
{status.indexOf('%') > -1 && <div className="Abort" onClick={onAbortClick}><span>×</span></div>}
</div>
</div>
);
};export default App;
File: /react-uploader/src/index.css
....Abort {
background: rgba(255, 0, 0, 0.5);
display: block;
position: absolute;
top: 0;
right: 0;
width: 50px;
height: 50px;
clip-path: polygon(0 0, 100% 100%, 100% 0);
transition: all 250ms ease-in-out 0s;
cursor: pointer;
}.Abort:hover {
background: rgba(255, 0, 0, 1);
}.Abort > span {
color: white;
font-family: 'Helvetica', Arial, sans-serif;
font-weight: bold;
font-size: 24px;
height: 28px;
width: 22px;
line-height: 28px;
position: absolute;
top: 0;
right: 0;
}
Now we can abort and reset our upload at anytime while a current file is uploading.
Going From Here
There are a few other things we do to improve things:
- Limit the file preview — that way it catches files that are too big to prevent them from bogging down the browser’s memory.
- Handle other data like xls and pdf — CSV files can be read natively, and PDF files can previewed in a specific way with certain browsers.
This was quite a long walkthrough, so congrats on reading this far.
If you got value from, please give this article praise 👏❤️.️
If you think this can be improved, please let me know in the comments.
Please share it on twitter 🐦 or other social media platforms. Thanks again for reading. 🙏
Please also follow me on twitter: @codingwithmanny and instagram at @codingwithmanny.