Learn the MERN stack - by building an Instagram clone (Part Two)
Tags: javascript, react
Published: 2022-03-13 11:20:37

In the [last article](https://dev.to/tylerasa/learn-the-mern-stack-by-building-an-instagram-clone-part-one-188h) we set up our backend, now we need a frontend need to connect to our backend.
It is the MERN stack so we will use React. 
But before anything let's start our server with `node server.js`.

Open your terminal and run `npm create-react-app <project-name-here>` to create a new react project. Replace `<project-name-here>` with the name of your project. 
After the react project is done installing, `cd` into the project directory, we will need to run `npm i react-router-dom axios`. We will use `axios` to make request to the backend and `react-router-dom` to allow route between different pages in our web app.
In our root project directory let's create `.env` and add the following

```
REACT_APP_BE=http://localhost:5000
```
This adds the value of our backend server which we can access anywhere in our code with `process.env.REACT_APP_BE`.

## Clean up
By default React comes with some boiler plate code in the `App.js` file, which we will not need so you can delete everything in between the brackets of the return method.

```jsx
//App.js
import "./App.css";

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

export default App;
```
Our `App.js` should look like this.
Now we have to set up the routes to the pages for logging and registering an account, but we have not any pages to point them to so let's go and create them.
In the src folder, create a new folder called `pages`.

## Login Page
In the pages folder create a new folder and name it `Login`. In the `Login` folder, create two files, `Login.js` and `styles.css`.

```jsx
//Login.js
import React, { useState } from "react";
import axios from "axios";
import "./styles.css";
import { useNavigate } from "react-router-dom";
const Login = () => {
  const navigate = useNavigate();

  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const handleSubmit = (e) => {
    e.preventDefault();
    var data = JSON.stringify({
      username,
      password
    });

    var config = {
      method: "post",
      url: `${process.env.REACT_APP_BE}/users/login`,
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json"
      },
      data: data
    };

    axios(config)
      .then(function (response) {
        localStorage.setItem("my_user_token", response.data.token);
        navigate("/home");
      })
      .catch(function (error) {
        console.log(error);
      });
  };
  return (
    <div className="login-wrapper">
      <form onSubmit={(e) => handleSubmit(e)}>
        <input
          onChange={(e) => setUsername(e.target.value)}
          placeholder="username"
          type="text"
        />
        <br />
        <input
          onChange={(e) => setPassword(e.target.value)}
          placeholder="password"
          type="password"
        />
        <br />
        <button>login</button>
        <a  href="/register" className="create-account">create an account</a>
      </form>
    </div>
  );
};

export default Login;

```
The above is pretty easy, let's break it down

1. We create a functional component for the Login page, with the input fields and button to login. There is also a link a register page which we will create later. We use the `onChange` event to listen for changes in the input fields and update the states.
2. In the `handleSubmit` function, we execute the `e.preventDefault()` to prevent the form from being submitted. Then we store the values of the username and password and add them to the config object which contain other information like the type of request we will like to make, the url we are making the request to and the headers required.
3. With the config object encapsulating all the data we need, we can make our post request. This the request is successful and we are able to login, we get back the user data along with the generate token which we will store in the brower's localStorage for use later.

``` css
/*styles.css*/
.login-wrapper{
    width: 100vw;
    height: 100vh;
    background-color: #222;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
}



input{
    outline: none;
    margin-bottom: 10px;
    width: 300px;
    padding: 10px;
    font-size: 16px;
    font-family: 'Courgette', cursive;
    font-family: 'Raleway', sans-serif;
    border-radius: 5px;
    border: 0;
}

button{
    background: rgb(77, 247, 43);
    border: 0;
    width: 100%;
    padding: 10px;
    border-radius: 5px;
    outline: none;
    margin: auto;
    font-family: 'Courgette', cursive;
    font-family: 'Raleway', sans-serif;
    font-weight: bold;
    cursor: pointer;
    margin-bottom: 10px;
}

.create-account{
    color: white;
    text-decoration: none;
    float: right;
}

.create-account:hover{
text-decoration: underline;
}
```
Styles for the `Login.js`.


## Sign up page

```jsx
//SignUp.js
import React, {useState} from "react";
import axios from "axios";
import { useNavigate } from "react-router-dom";
const SignUp = () => {

  const navigate = useNavigate();

  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const handleSubmit = (e) => {
    e.preventDefault();
    var data = JSON.stringify({
      username,
      password
    });

    var config = {
      method: "post",
      url: `${process.env.REACT_APP_BE}/users/register`,
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json"
      },
      data: data
    };

    axios(config)
      .then(function (response) {
        localStorage.setItem("my_user_token", response.data.token);
        navigate("/home");
      })
      .catch(function (error) {
        console.log(error);
      });
  };
  return (
    <div className="login-wrapper">
      <form onSubmit={(e) => handleSubmit(e)}>
        <input
          onChange={(e) => setUsername(e.target.value)}
          placeholder="username"
          type="text"
        />
        <br />
        <input
          onChange={(e) => setPassword(e.target.value)}
          placeholder="password"
          type="password"
        />
        <br />
        <button>register</button>
        <a href="/" className="create-account">
          already have an account
        </a>
      </form>
    </div>
  );
};

export default SignUp;
```
In the pages folder, create a SignUp folder and create a `SignUp.js` file. The logic here is the same as the one in the `Login.js` file, where we get the username and password and make a post request to the `/register` route and navigate to the `Home` page if the credentials are valid.
 
## Defining the Home page and defining the routes for all the pages
Now that we have designed pages for users to login and register, we need to set up a `Home` page to route users to after a successful request. In the pages folder create a `Home` folder and create a `Home.js` and `styles.css` file.

```jsx
//Home.js
import React from "react";
import "./styles.css";

const Home = () => {
  
  return (
    <div>Home</div>
  );
};

export default Home;
```
The code above creates a default `Home.js` file. We will come back to the `Home` file later, now let's go back to our `App.js` file and define the routes for the individual pages.

```jsx
import { BrowserRouter, Route, Routes } from "react-router-dom";
import "./App.css";
import Home from "./pages/Home/Home";
import Login from "./pages/Login/Login";
import SignUp from "./pages/SignUp/SignUp";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route exact path="/"  element={<Login/>} />
        <Route exact path="/register" element={<SignUp/>} />
        <Route exact path="/home" element={<Home/>} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;
```
We import the `BrowserRouter`, `Route` and `Routes` from `react-router-dom` and also import the individual files themselves. Now we specify the our initial route should be a the `Login` page, `/register` for the `SignUp` and `/home` for `Home`. 

## Back to the Home page
Before we continue with the Home page, we need to know how our Home is going to look like. On the Home page: 

1. User should see their posts and other posts by other users. 
2. Add a new post.
3. Also add comment on posts.

When the user gets routed to the Home page, they will see all the posts added by others, so we need to make a request to the `/posts` to get all post in the database.
```jsx
//Home.js
import React, { useEffect, useState } from "react";
import axios from "axios";
import "./styles.css";

const Home = () => {
  const [posts, setPosts] = useState([]);
  const [userName, setUsername] = useState("");


  useEffect(() => {
    var token = localStorage.getItem("my_user_token");
    var base64Url = token.split(".")[1];
    var base64 = base64Url.replace("-", "+").replace("_", "/");
    setUsername(JSON.parse(atob(base64)).username);
    var config = {
      method: "get",
      url: `${process.env.REACT_APP_BE}/posts`,
      headers: {
        Authorization: `Bearer ${localStorage.getItem("my_user_token")}`
      }
    };

    axios(config)
      .then(function (response) {
        setPosts(response.data);
      })
      .catch(function (error) {
        navigate("/");
        console.log(error);
      });
  }, []);
  
  return (
    <div>Home</div>
  );
};

export default Home;
```
In the above code, when the user gets routed to we make a request to the `/posts` routes to get the all the posts in the database. With posts we got from the database we can map from the posts and pass each one as props to our `PostCard` component. Remember when we sign the username and id when creating the token, now we use 

```javascript 
    var base64 = base64Url.replace("-", "+").replace("_", "/");
    setUsername(JSON.parse(atob(base64)).username);
```
to decode the token payload and set the username state to the extracted username. 

All these will be the Home page, so will need component to manage each of these action want the user to perform. A `PostCard` component to display user posts, a `Comments` component view to view comments on posts and also add a new comment. One last component to help us add a new post, `AddPost`.


## PostCard component
We will first start with the `PostCard` component. 
In the `src` folder, create a `components` folder and create a new `PostCard` folder. In the `PostCard` folder, create two files, one for `PostCard.js` and the other `styles.css`.

```javascript
//PostCard.js
import React from "react";
import profile from "../../assets/images/profile.jpg";


import "./styles.css";
const PostCard = ({ post }) => {
 

  return (
    <div className="post-card">
      <div className="post-card-header">
        <img src={profile} className="avatar" />
        {post.username}
      </div>
      <img src={post.image} alt={post.caption} className="post-image" />
      <div className="post-card-body">
        <span className="username">{post.username} </span>
        <span className="post-caption">{post.caption}</span>
        {post.comments.length > 0 ? (
          <p className="view-comments">
            View all comments
          </p>
        ) : (
          <p className="view-comments">
            No comments yet
          </p>
        )}
      </div>
    </div>
  );
};
export default PostCard;
```
Later when pass our post in the `PostCard` component, we can get the `username`, `caption` and `image` associated with that post. We also import a default profile image to use as the avatar for each user.
Below is the styles for the post card.

```css
/*styles.css*/
.wrapper {
  max-width: 900px;
  margin: auto;
  display: grid;
  grid-template-columns: 550px auto;
  border: 1px solid #f3f3f3;
}

.left-side {
  margin-top: 40px;
}

.right-side {
  margin-top: 40px;
}

.header {
  width: 100%;
  background-color: rgb(0, 255, 187);
  height: 40px;
  position: fixed;
}

.user-profile-wrapper {
  background-color: white;
  padding: 15px;
  font-weight: bold;
  margin-top: 20%;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.user-profile {
  display: flex;
  align-items: center;
  justify-content: center;
}
.logout {
  cursor: pointer;
  color: rgb(0, 57, 172);
}

.avatar-lg {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  margin-right: 10px;
}

.inner-header {
  max-width: 900px;
  margin: auto;
  display: grid;
  grid-template-columns: 550px;
  text-align: right;
}

@media only screen and (max-width: 768px) {
  .wrapper {
    grid-template-columns: auto;
  }

  .user-profile-wrapper {
    border: 1px solid #f0ebeb;
    padding: 5px;
    flex-grow: 1;
  }
  .right-side {
    position: fixed;
    left: 0;
    bottom: 0;
    width: 100%;
  }

  .avatar-lg {
    width: 30px;
    height: 30px;
  }
  .header {
    display: flex;
    justify-content: flex-end;
  }
  .inner-header {
    padding-right: 10px;
  }
}

```

Now that we have `PostCard` ready we can loop through our array of posts returned from the backend and pass them as props into the `PostCard`.
Before our state gets complicated let create a context file to make a state management easy.
In the components folder lets create a `showContext.js` file 

```javascript
//showContext.js
import React from "react";
export const ShowContext = React.createContext({});
```
The context will help manage our state and make them accessible to all children in the context provider.

```javascript
//Home.js
import axios from "axios";
import React, { useEffect, useState } from "react";
import Comments from "../../components/Comments/Comments";
import PostCard from "../../components/PostCard/PostCard";
import { ShowContext } from "../../components/showContext";
import "./styles.css";
import { useNavigate } from "react-router-dom";
import AddPost from "../../components/AddPost/AddPost";
const Home = () => {
  const navigate = useNavigate();
  const [showComments, toggleComments] = useState(false);
  const [showAddPosts, toggleAddPost] = useState(false);
  const [posts, setPosts] = useState([]);
  const [userName, setUsername] = useState("");
  useEffect(() => {
    var token = localStorage.getItem("my_user_token");
    var base64Url = token.split(".")[1];
    var base64 = base64Url.replace("-", "+").replace("_", "/");
    setUsername(JSON.parse(atob(base64)).username);
    var config = {
      method: "get",
      url: `${process.env.REACT_APP_BE}/posts`,
      headers: {
        Authorization: `Bearer ${localStorage.getItem("my_user_token")}`
      }
    };

    axios(config)
      .then(function (response) {
        setPosts(response.data);
      })
      .catch(function (error) {
        navigate("/");
        console.log(error);
      });
  }, [showAddPosts]);

  const handleLogout = () => {
    localStorage.removeItem("my_user_token");
    navigate("/");
  };
  return (
<ShowContext.Provider
 value={{
        comments: [showComments, toggleComments],
        add: [showAddPosts, toggleAddPost]
      }}
>
        <div className="wrapper">
          <div className="left-side">
            {posts.map((ele, i) => {
              return <PostCard post={ele} key={i} />;
            })}
          </div>
        </div>
    </ShowContext.Provider>
  );
};

export default Home;
```
Since we going to have a component to make comments and another to add posts, we create the context to pass values from the top down the individual components.
We wrap the return jsx with a context provider and pass in the values of `comments` and `add`. Comments 
```css
/*styles.css*/
.wrapper {
  max-width: 900px;
  margin: auto;
  display: grid;
  grid-template-columns: 550px auto;
  border: 1px solid #f3f3f3;
}

.left-side {
  margin-top: 40px;
}

```
Now our `Home.js` should look like this. We loop through the `posts` state and we have also some few classes to style the page.
![example of post card](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/toh1p2m5094ac7h4miru.png)
Our home page should look something like this if have posts in your database. 


Right now we can only see the posts we made in the previous tutorial, but before we user provide users with a button to add new posts. Let's make sure they can add comments on existing posts.

## Commenting on posts

```javacript
//Comments.js
import React, { useContext, useRef, useEffect, useState } from "react";
import "./styles.css";
import profile from "../../assets/images/profile.jpg";
import { ShowContext } from "../showContext";
import UserComment from "./UserComment";
import axios from "axios";
const Comments = () => {
  const { comments } = useContext(ShowContext);
  const [showComments, toggleComments] = comments
  const [clickState, setClickState] = useState(false);
  const [content, setContent] = useState("");
  const cardRef = useRef();
  console.log(showComments);
  useEffect(() => {
    function handleClickOutside(event) {
      if (cardRef.current && !cardRef.current.contains(event.target)) {
        toggleComments({
          status: false,
          post: null
        });
      }
    }

    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [clickState, toggleComments]);

  const handleSubmit = (e) => {
    e.preventDefault();
    var postId = showComments.post._id;
    var token = localStorage.getItem("my_user_token");
    var base64Url = token.split(".")[1];
    var base64 = base64Url.replace("-", "+").replace("_", "/");
    var userId = JSON.parse(atob(base64)).id;

    var data = JSON.stringify({
      content
    });
    var config = {
      method: "post",
      url: `${process.env.REACT_APP_BE}/posts/add-comment/${postId}/${userId}`,
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${localStorage.getItem("my_user_token")}`
      },
      data: data
    };

    axios(config)
      .then(function (response) {
        console.log(JSON.stringify(response.data));
      })
      .catch(function (error) {
        console.log(error);
      });
  };

  return (
    <div onClick={() => setClickState(!clickState)} className="comments-modal">
      <div ref={cardRef} className="comment-card">
        <div
          className="comment-img"
          style={{
            background: `url(${showComments.post.image})`,
            backgroundRepeat: "no-repeat",
            backgroundPosition: "center",
            backgroundSize: "cover"
          }}
        ></div>
        <div className="comments-main">
          <div className="post-card-header">
            <img src={profile} className="avatar" />
            {showComments.post.username}
          </div>
          {showComments.post.comments.map((ele, i) => {
            return <UserComment key={i} item={ele} />;
          })}
          <form onSubmit={(e) => handleSubmit(e)} className="form">
            <input
              onChange={(e) => setContent(e.target.value)}
              placeholder="say something..."
              className="form-input"
              type="text"
            />
          </form>
        </div>
      </div>
    </div>
  );
};
export default Comments;
```
Let break the code.
Our comment component is going to be modal with black overlay, that has consist of a grid with the image of the post we are commenting on the left and the other comments on the right.

1. The root div of the component a function `setClickState` to close the modal whenever the user clicks outside the comment box(with ref of `cardRef`) or the in the black overlay.
2. The div is a grid container of the post image and the comments section with the list all comments.
so now need to allow users to able to add new posts.
3. When we submit our comment to hit the `/post/add-comment` endpoint passing in the id of the post and the id of the user.

## Add new posts
In the header div in on our Home page, we will add a button to add a new button.

```javascript
//AddPost.js
import React, { useRef, useEffect, useState, useContext } from "react";
import axios from "axios";
import FileBase64 from "react-file-base64";
import "./styles.css";
import { ShowContext } from "../showContext";
const AddPost = () => {
  const cardRef = useRef();

  const { add } = useContext(ShowContext);
  const [showAddPosts, toggleAddPost] = add;
  const [clickState, setClickState] = useState(false);
  const [picture, setPicture] = useState(null);
  const [caption, setCaption] = useState("");
  const [showError, setShowError] = useState(false);
  useEffect(
    () => {
      function handleClickOutside(event) {
        if (cardRef.current && !cardRef.current.contains(event.target)) {
          toggleAddPost(!showAddPosts)
        }
      }
      document.addEventListener("mousedown", handleClickOutside);
      return () => {
        document.removeEventListener("mousedown", handleClickOutside);
      };
    },
    [clickState]
  );
  function getFile(file) {
    var exp = /\d+/;
    if (file.size.match(exp)[0] > 100) {
      setShowError(true);
    } else {
      setShowError(false);
      setPicture(file);
    }
  }

  const handleSubmit = e => {
    e.preventDefault();
    var token = localStorage.getItem("my_user_token");
    var base64Url = token.split(".")[1];
    var base64 = base64Url.replace("-", "+").replace("_", "/");
    var userId = JSON.parse(atob(base64)).id;

    var data = JSON.stringify({
      caption,
      image: picture.base64
    });

    var config = {
      method: "post",
      url: `${process.env.REACT_APP_BE}/posts/add/${userId}`,
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${localStorage.getItem("my_user_token")}`
      },
      data: data
    };

    axios(config)
      .then(function(response) {
            toggleAddPost(!showAddPosts);

      })
      .catch(function(error) {
        console.log(error);
      });
  };
  return (
    <div onClick={() => setClickState(!clickState)} className="comments-modal">
      <div ref={cardRef} className="comment-card">
        <div
          className="comment-img add-post"
          style={{
            backgroundRepeat: "no-repeat",
            backgroundSize: "contain",
            backgroundPosition: "center",
            background: picture ? `url(${picture.base64})` : null
          }}
        >
          {showError && <p className="error">File must be less 100kb</p>}
          {!picture
            ? <FileBase64 onDone={getFile} />
            : <span onClick={() => setPicture(null)} className="remove-button">
                x
              </span>}
        </div>

        <div className="comments-main">
          <form onSubmit={e => handleSubmit(e)} className="form">
            <input
              onChange={e => setCaption(e.target.value)}
              placeholder="say something..."
              className="form-input"
              type="text"
            />
          </form>
        </div>
      </div>
    </div>
  );
};

export default AddPost;
```
The `AddPost` component looks like Comments modal with a grid layout for one the image and one for the comments. 
The user click on the choose file button to add an image and after they can type in the input field to caption the post.
On submit we hit `/posts/add/` endpoint and also check the file size if below `100kb`.

Now we can add new post and also make comments on existing posts.


## Finishing up

```
<ShowContext.Provider
      value={{
        comments: [showComments, toggleComments],
        add: [showAddPosts, toggleAddPost]
      }}
    >
      <div>
        {showComments.status ? <Comments /> : null}
        {showAddPosts ? <AddPost /> : null}
        <div className="header">
          <div onClick={() => toggleAddPost(true)} className="inner-header">
            new post
          </div>
        </div>
        <div className="wrapper">
          <div className="left-side">
            {posts.map((ele, i) => {
              return <PostCard post={ele} key={i} />;
            })}
          </div>
          <div className="right-side">
            <div className="user-profile-wrapper">
              <div className="user-profile">
                {userName}
              </div>
              <span onClick={handleLogout} className="logout">
                logout
              </span>
            </div>
          </div>
        </div>
      </div>
    </ShowContext.Provider>
```
Because the `Comment` and `AddPost` components are modal we can display all at once on the page. We only need to appear when a condition is met. So the `Comments` will show when the `showComments.status` is true and `AddPost` will show when the `showAddPosts` is true.
We also need a logout button.

###Update the Home styles.css

```
.wrapper {
  max-width: 900px;
  margin: auto;
  display: grid;
  grid-template-columns: 550px auto;
  border: 1px solid #f3f3f3;
}

.left-side {
  margin-top: 40px;
}

.right-side {
  margin-top: 40px;
}

.header {
  width: 100%;
  background-color: rgb(0, 255, 187);
  height: 40px;
  position: fixed;
}

.user-profile-wrapper {
  background-color: white;
  padding: 15px;
  font-weight: bold;
  margin-top: 20%;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.user-profile {
  display: flex;
  align-items: center;
  justify-content: center;
}
.logout {
  cursor: pointer;
  color: rgb(0, 57, 172);
}

.avatar-lg {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  margin-right: 10px;
}

.inner-header {
  max-width: 900px;
  margin: auto;
  display: grid;
  grid-template-columns: 550px;
  text-align: right;
  cursor: pointer;
}

@media only screen and (max-width: 768px) {
  .wrapper {
    grid-template-columns: auto;
  }

  .user-profile-wrapper {
    border: 1px solid #f0ebeb;
    padding: 5px;
    flex-grow: 1;
  }
  .right-side {
    position: fixed;
    left: 0;
    bottom: 0;
    width: 100%;
  }

  .avatar-lg {
    width: 30px;
    height: 30px;
  }
  .header {
    display: flex;
    justify-content: flex-end;
  }
  .inner-header {
    padding-right: 10px;
  }
}
```
Now our code should be working perfectly. You can expand the currently feature and a like button and other stuff.

Check the out the full code [here.](https://github.com/Tylerasa/secretion-network)
Check the demo [here.](https://secretion-network-jce9gx50u-tylerasa.vercel.app)