[React + Node.js]: Create your MERN Stack Application - Part 2: Frontend
React
Node.js
03/12/2020
Overview
This post is continuation of this post to create a MERN (MongoDB, Express.js, React, Node.js) Stack Application. Part 2 will focus on setting up the client side with React. If you haven't previously set up the MongoDB, Express.js, Node.js, please go through the Part 1 first.
Front-end Set Up (React)
Go to the root directory, mern-demo, and we'll develop our front-end code in a directory front-end. In the mern-demo root directory, install create-react-app.
npx create-react-app front-endCurrent file structure
mern-demo  ├── front-end   |     └── ...all client side codes   ├── controllers  |     └── user.js  ├── routes  |     └── user.js  ├── models  |     └── user.js  ├── app.js  ├── config.js  └── package.json
create-react-appcreates.git-ignorefile infront-enddirectory. If you plan to push anything on a repository, move.git-ignoreout offront-endand place it undermern-demo, root directory. i.e.mv front-end/.git-ignore .By doing so,.git-ignorewill be applied to the whole project.
package.json
Open back mern-demo/package.json to add more scripts--you do not need to modify mern-demo/front-end/package.json.
"scripts": {    "start": "node app.js",    "server": "nodemon app.js",    "install-client": "npm i --prefix front-end",     "client": "npm start --prefix front-end",     "dev": "concurrently \"npm run server\" \"npm run client\"" },
npm run install-clientwill install npm packages insidemern-demo/front-end/package.json. This is equivalent tocd front-end && npm i
npm run clientis equivalent to runningcd front-end && npm start
npm run devthis command will now run both front and back end servers concurrently
.env & .env.production
mern-demo  ├── front-end   |     ├── .env.production   |     ├── .env   |     └── ...  └── ...Create .env to contain environmental variables. If you're going to deploy your app later, create .env.production as well. Remember to add .env* inside .git-ignore if you're uploading your project somewhere.
front-end/.env
REACT_APP_FETCH_URL=http://localhost:8080front-end/.env.production
REACT_APP_FETCH_URL=<YOUR_URL_ON_PRODUCTION>React will use variables in
.envon development, but onnpm run build, it will grab variables from.env.productioninstead.After you save the variable inside
.env*, you need to restart the server for it to take effect
front-end/src/..
Inside front-end/src, we'll create a few components: CustomButton, User, UserAddForm. Then, tweak the default files App.js and App.css for styling.
mern-demo  ├── front-end  |     ├── src  |     |    ├── App.css   |     |    ├── App.js   |     |    ├── custom-button.jsx   |     |    ├── user-add-form.jsx    |     |    ├── user.jsx   |     |    └── ...  |     ├── .env.production  |     ├── .env  |     └── ...  └── ...user.jsx
import React from "react"import CustomButton from "./custom-button"
const User = ({ id, name, age, occupation, attr, handleDelete, loading }) => {  return (    <li className={`${attr ? "attr" : ""} user`}>      <span>{name}</span>      <span>{age}</span>      <span>{occupation}</span>      {!attr && (        <CustomButton remove onClick={() => handleDelete(id)} loading={loading}>          X        </CustomButton>      )}    </li>  )}
export default Usercustom-button.jsx
import React from "react"const CustomButton = ({ children, remove, loading, disabled, ...others }) => {  return (    <button      className={`${remove ? "btn-delete" : ""} btn-custom`}      disabled={disabled || loading}      {...others}    >      {children}    </button>  )}export default CustomButtonuser-add-form.jsx
import React, { useState } from "react"import CustomButton from "./custom-button"
const FormInput = ({ ...otherProps }) => {  return (    <div className="group">      <input        className={"input-custom"}        type="text"        {...otherProps}        onChange={e => otherProps.onChange(otherProps.id, e.target.value)}      />    </div>  )}
const USER_FORM = {  name: "",  age: "",  occupation: "",}
const UserAddForm = ({ loading, handleAdd }) => {  const [userForm, setUserForm] = useState(USER_FORM)
  const handleChange = (input, value) => {    const updatedForm = {      ...userForm,      [input]: value,    }    setUserForm(updatedForm)  }
  const handleSubmit = e => {    e.preventDefault()    handleAdd(userForm)    setUserForm(USER_FORM)  }
  return (    <form className="form-add-user" onSubmit={e => handleSubmit(e)}>      <FormInput        id="name"        name="name"        placeholder="name"        value={userForm["name"]}        onChange={handleChange}      />      <FormInput        id="age"        name="age"        placeholder="age"        value={userForm["age"]}        onChange={handleChange}      />      <FormInput        name="occupation"        id="occupation"        placeholder="occupation"        value={userForm["occupation"]}        onChange={handleChange}      />      <CustomButton loading={loading} type="submit">        Add      </CustomButton>    </form>  )}
export default UserAddFormApp.js
import React, { useState, useEffect } from "react"import User from "./user"import UserAddForm from "./user-add-form"import "./App.css"
const App = () => {  const [addLoading, setAddLoading] = useState(false)  const [deleteLoading, setDeleteLoading] = useState(false)  const [users, setUsers] = useState([])
  useEffect(() => {    fetchUsers()  }, [])
  const fetchUsers = async () => {    try {      const res = await fetch(process.env.REACT_APP_FETCH_URL + "/user", {        method: "GET",      })      if (res.status !== 200) {        throw new Error("Fetching user failed")      }      const resData = await res.json()      setUsers(resData.users)    } catch (err) {      console.log(err)    }  }
  const handleAdd = async formData => {    setAddLoading(true)    try {      const res = await fetch(process.env.REACT_APP_FETCH_URL + "/user", {        method: "POST",        headers: {          "Content-Type": "application/json",        },        body: JSON.stringify({          name: formData.name,          age: formData.age,          occupation: formData.occupation,        }),      })      if (res.status !== 200 && res.status !== 201) {        throw new Error("Adding user failed")      }      const resData = await res.json()      const updatedUsers = [resData.user, ...users]      setUsers(updatedUsers)    } catch (err) {      console.log(err)    }    setAddLoading(false)  }
  const handleDelete = async userId => {    setDeleteLoading(true)    try {      const res = await fetch(        process.env.REACT_APP_FETCH_URL + "/user/" + userId,        {          method: "DELETE",        }      )      if (res.status !== 200) {        throw new Error("Deleting user failed")      }      await res.json()      const updatedUsers = users.filter(user => user._id !== userId)      setUsers(updatedUsers)    } catch (err) {      console.log(err) ///    }    setDeleteLoading(false)  }
  return (    <div className="App">      <header>        <h1>MERN Stack App Demo</h1>        <hr />      </header>      <main>        <h2>Add User:</h2>        <UserAddForm loading={addLoading} handleAdd={handleAdd} />        <hr />        <h2>Users:</h2>        <ul className="user-list">          <User name={"Name"} age={"Age"} occupation={"Occupation"} attr />          {users.length > 0 ? (            <>              {users.map(user => (                <User                  key={user._id}                  id={user._id}                  name={user.name}                  age={user.age}                  occupation={user.occupation}                  handleDelete={handleDelete}                  loading={deleteLoading}                />              ))}            </>          ) : null}        </ul>      </main>    </div>  )}
export default AppApp.css
.App {  max-width: 468px;  padding: 1rem;  background: #eee;  margin: auto;  font-family: sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto;}
.App .user-list {  list-style: none;  padding: 1rem 0 1rem 2rem;  display: flex;  flex-direction: column;  background: #ddd;  text-align: left;}
/* User */li.user {  position: relative;  display: grid;  grid-template-columns: 1fr 1fr 1fr;  align-items: center;  padding: 1rem;  opacity: 0.1;  animation: fadeOut 500ms forwards ease-in;}
li.user:hover {  background: #ccc;}
li.attr {  font-weight: bold;  border-bottom: 1px solid #aaa;}
li.attr:hover {  background: none;}
/* Form */.form-add-user {  display: flex;  justify-content: center;  align-items: center;  flex-wrap: wrap;}
/* Input */.input-custom {  padding: 0.5rem;  text-align: center;  margin: 0.5rem 0.3rem;}
/* Buttons */.btn-custom {  cursor: pointer;  padding: 0.3rem 0.5rem;  background: #ffd700;  border: 1px solid #d8b72f;  color: #333;  border-radius: 5px;}.btn-delete {  position: absolute;  left: -20px;}.btn-custom:hover {  color: #ffd700;  background: #333;  border-color: transparent;}.btn-custom:disabled {  cursor: not-allowed;  background: #ccc;  color: #888;  border: #ccc;  box-shadow: none;}
.btn-custom:disabled:hover,.btn-custom:disabled:active {  background: #ccc;  color: #888;  border: #ccc;}
@keyframes fadeOut {  to {    opacity: 1;  }}What's next? 🤔
- [React + Node.js]: Deploy Your MERN Stack App on Heroku
- [React + Node.js]: Deploy Your MERN Stack App to Amazon EC2 with SSL Encryption
- [React + Node.js]: Implement Web Socket with Socket.io
- [React + Node.js]: Create your MERN Stack Application - Part 1: Backend
- [React + Node.js]: Deploy Your MERN Stack App on Heroku