Full Stack Instagram: Post Upload
Tags: react, firebase, javascript, webdev
Published: 2021-05-15 05:44:19

## Table of contents:

- Demo
- Overview
- Setting up Firebase
- Post Upload
- Progress Bar
- Post Download
- Image Compression

# Demo

You can check on the full source code and try them in Replit.

Repl url: [https://replit.com/@arnoldschan/PostUpload](https://replit.com/@arnoldschan/PostUpload)

![Apr-28-2021_23-04-50](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y4eabt7372tlaqh9cac4.gif)

# Overview

## User flow

1. As a user, they can upload a post by:
    - Choose a picture
    - Fill in the caption
    - Hit the upload button
2. The progress bar shows the uploading process.
3. The new uploaded post will show in the timeline
4. User can see all of the uploaded posts

## File tree:
![Untitled](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s0c7oe0tnk825k986rwj.png)

This is how the project file tree looks like.

- The main `App.jsx` is in root folder
- The smaller components in `components/` folder
- Components' css in `css/` folder
- Anything related to firebase is inside `firebase/`folder

# Setting up Firebase

You can follow the setting up firebase project guidelines [here](https://dev.to/arnoldschan/full-stack-react-how-to-build-your-own-instagram-with-react-js-firebase-authentication-guest-m2f). After you have set up the project, initiate firebase modules in `firebase.js`:

```jsx
//firebase.js
import firebase from 'firebase';
import firebaseConfig from "./firebaseConfig";
const firebaseApp = firebase.initializeApp(
    firebaseConfig
)
const db = firebaseApp.firestore();
const storage = firebaseApp.storage();

export { db, storage }
```

The Firestore and Firebase Storage can be used directly without configuring anything in Firebase portal. You may need to change the security rules to open the database for the public (unless you want to implement authentication).

# Post Upload

## Choose a picture

```jsx
//PostUpload.jsx
const [file, setFile] = useState(null)
//...
const chooseFile = (e) => {
        if (e.target.files[0]){
            setFile(e.target.files[0]);
        }
    }
//...
<Input 
	id="fileinput"
	className="child"
	type="file"
	name="upload-file"
	onChange={chooseFile}
/>
```

The user interacts with `<Input/>` element to open up the file explorer pop-up. If the user chooses a file, `chooseFile` function will be triggered. 

In this function, the `file` state hook will be updated with the chosen file information.

## Fill in the caption

```jsx
//PostUpload.js
//...
const [caption, setCaption] = useState("")
//...
<Input 
	className="child"
	type="text"
	name="upload-caption"
	placeholder="write your caption here"
	value={caption}
	onChange={(e)=>setCaption(e.target.value)}
/>
//...
```

The user can write their caption through the input box in `<Input/>` element. In any letter the user input, the `caption` state hook will be get updated as well.

## Upload to Firebase

```jsx
// PostUpload.jsx
import { db, storage } from '../firebase/firebase';
//...
const [progress, setProgress] = useState(0)
// this state is updated by chooseFile function
const [file, setFile] = useState(null)
//...
const uploadFile = () => {
     if (file === null){
        alert('Please choose a file!')
     return;
     };
     const imageName = file.name;

     //Store the post Information into Firestore
     const addPost = function(caption, username, url) {
     const newPost = {
              timestamp: firebase
                          .firestore
                          .FieldValue
                          .serverTimestamp(),
              caption: caption,
              username: username,
              imageURL: url
          }
          db.collection('posts').add(newPost)
      };
      // Save the uploaded picture into Firebase storage
      const uploadTask = storage
                           .ref(`images/${imageName}`)
                           .put(file)
//...

<Button className="child" onClick={uploadFile}>Upload</Button>
```

The post upload separated into two main things:

- store the post information (caption, username, etc) into Firestore. Handled by `addPost` .
- save the uploaded picture into Firebase storage. This task is done by `uploadTask`.

### Store the post Information into Firestore

`newPost` defines the post information that we want to store in Firestore. There are 4 things that we want to know for each post:

- `timestamp` : the value is obtained from the firebase library. This represents the upload time
- `caption` : obtained from user input for this post's caption
- `username` : I put the value as `uploader` in this example. However, in our main project, this contains the information of logged in user.
- `imageURL`: This post's uploaded picture URL in Firebase storage. We will get the value after the picture has been successfully uploaded.

```jsx
db.collection('posts').add(newPost)
```

We can simply call the code above to add our data into Firestore. 

`db.collection('posts')`  specifies which collection that we are referring to. In this example, I store the post's information into the "posts" collection. 

Then, we can add our new post into this collection by chaining the collection with `add` method plus `newPost` that we just defined previously as the argument. 

Notice that we only declare this function and haven't called it yet. We want the post information to be stored only if the picture uploading process has been finished. 

I will mention this `addPost` function again later in the progress bar section.

### Save the uploaded picture into Firebase storage

We cannot only use Firestore in this example. Firestore only supports text-based information. The uploaded picture is needed to be stored somewhere else. We will use Firebase storage in this example.

```jsx
storage.ref(`images/${imageName}`).put(file)
```

The uploaded picture information is already stored in our `file` state hook. We can use `storage.ref` to tell which target directory and filename in the storage. In this example, I chose `images/{filename}` as the file reference. We can then chain this with `put` method and use `file` as the argument.

# Progress Bar

The upload process may need to take some time to be finished, depending on the picture size and internet speed. For a better user experience, we can give a visual hint on how the upload process is going. One of the best ways is through the progress bar.

Firebase storage supports these needs by checking on how many bytes that the picture has been transferred.

```jsx
//PostUpload.jsx
//...
const [progress, setProgress] = useState(0)
//...
    uploadTask.on(					 
       "state_changed",					 
       (snapshot) => {							 
           const progressNum = Math.round(							 
           (snapshot.bytesTransferred/ snapshot.totalBytes)* 100							 
           );							 
           setProgress(progressNum);
       },
       (error) => {
           console.log(error);
           alert(error.message);
       },
       () => {
           storage
             .ref('images')
             .child(imageName)
	     .getDownloadURL()
	     .then(url => {
                addPost(caption, username, URL)
       })
```

Notice that we reuse the upload task that we previously stored in `uploadTask`. In every `state_changed` of the uploading task, the `progress` state hook will be get updated. The value of this `progress` is calculated by: `bytesTransferred` of the snapshot divided by the `totalBytes` of the uploaded picture. 

After the image has been completely uploaded, the second callback function is triggered.  Remember `addPost` function we have defined previously? Here, the post information is stored in Firebase along with its uploaded picture URL.

# Post Download

Besides post uploading, users can also see all of the uploaded posts in the system. Previously, I experimented with the real-time connection in Firebase, however, I can't find a way to paginate and limit the query. I ended up using a simple query and limit the post on every page.

```jsx
//App.jsx
import { db } from "./firebase/firebase";
//...
const [posts, setPosts] = useState([])
//...
const fetchData = () => {
  db
  .collection('posts')
  .orderBy('timestamp','desc')
  .limit(10)
  .get().then(snapshot=>{
    if (snapshot.docs.length === 0);
    setPosts([...posts, ...snapshot.docs.map(doc=> (
      {id: doc.id,
        post: doc.data()}
        ))])
      })
	}
  useEffect(() => {
    fetchData();
  }, [])
```

All of the posts is stored in `posts` state hook. We retrieve the documents  by `get` method from "posts" collection, order them by "timestamp" attribute descendingly and limit the query by 10 posts.  `get` method returns an Async Promise so we need to chain it with updating the `posts` function. 

In this example, we only call `fetchData` once, when the user opens the app. In the latter example, We can update it in every user scroll.

# Image Compression

Yay! We've implemented all of the needed features for uploading posts. But hey, Upload the picture and download the picture need to take some time to be finished. What's wrong? Turns out the picture we upload and download follows the original size from the uploader. We can see this as another room for improvement.

You can see the difference in picture loading before and after compression:

![ezgif.com-gif-maker](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rktg1lsl1pe6feg452ap.gif)

The picture on the left takes a longer time to load than the picture on the right. The picture compression reduces the load time by almost half the time.

Here's how I compress the picture (disclaimer: it's a modified script from [stackoverflow](https://stackoverflow.com/questions/19262141/resize-image-with-javascript-canvas-smoothly)):

```jsx
// resizer.js
async function resizeMe(img) {
		var max_width = 500;
		var max_height = 500;

		var canvas = document.createElement('canvas');
		const bitmap = await createImageBitmap(img)
		var width = bitmap.width;
		var height = bitmap.height;

		// calculate the width and height, constraining the proportions
		if (width > height) {
			if (width > max_width) {
				height = Math.round(height *= max_width / width);
				width = max_width;
			}
		} else {
			if (height > max_height) {
				width = Math.round(width *= max_height / height);
				height = max_height;
			}
		}
		// resize the canvas and draw the image data into it
		canvas.width = width;
		canvas.height = height;
		var ctx = canvas.getContext("2d");
		ctx.drawImage(bitmap, 0, 0, width, height);
		var blobBin = atob(canvas.toDataURL("image/jpeg", 0.7).split(',')[1]);
		var array = [];
		for(var i = 0; i < blobBin.length; i++) {
				array.push(blobBin.charCodeAt(i));
		}
		var file = new Blob([new Uint8Array(array)], {type: 'image/png'});

		return file; // get the data from canvas as 70% JPG (can be also PNG, etc.)
	
	}

export default resizeMe;
```

This script creates a hidden `canvas` element to resize the picture. Here we set the max-width and the max height to 500. The script keeps the aspect ratio of the image and gets 70% of the quality of the resized picture. Because we need to wait for the image to be resized before passing the image to the uploading process, we need to set this function as an async function.

We then can simply call this function in our `PostUpload.jsx` . Don't forget, to put `async` in the `uploadFile` to await call `resizer` function. 

```jsx
// PostUpload.jsx
import resizer from "../utils/resizer";
// ...
const uploadFile = async () => {
// ...
	const uploadTask = storage.ref(`images/${imageName}`).put(await resizer(file))
//...
}
```

# Conclusion

Woo hoo! If you've reached this point, we have created a powerful post sharing web app!

Implementing the app is a breeze with the help of Firebase Firestore and Storage. In this example, we can see how Firebase Firestore helps us to store posts' information in JSON format, and store the image link from Firebase storage.

We also optimized the web app by compressing the picture size before it's get uploaded to shorten the picture upload and download process.

Do you have any better way to upload/ download  and resize the picture? Leave your solution below!