[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.

BASH
npx create-react-app front-end

Current file structure

C
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-app creates .git-ignore file in front-end directory. If you plan to push anything on a repository, move .git-ignore out of front-end and place it under mern-demo, root directory. i.e. mv front-end/.git-ignore . By doing so, .git-ignore will 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.

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-client will install npm packages inside mern-demo/front-end/package.json. This is equivalent to cd front-end && npm i

npm run client is equivalent to running cd front-end && npm start

npm run dev this command will now run both front and back end servers concurrently

.env & .env.production

C
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

TEXT
REACT_APP_FETCH_URL=http://localhost:8080

front-end/.env.production

TEXT
REACT_APP_FETCH_URL=<YOUR_URL_ON_PRODUCTION>

React will use variables in .env on development, but on npm run build, it will grab variables from .env.production instead.

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.

C
mern-demo
├── front-end
| ├── src
| | ├── App.css
| | ├── App.js
| | ├── custom-button.jsx
| | ├── user-add-form.jsx
| | ├── user.jsx
| | └── ...
| ├── .env.production
| ├── .env
| └── ...
└── ...

user.jsx

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 User

custom-button.jsx

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 CustomButton

user-add-form.jsx

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 UserAddForm

App.js

JSX
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 App

App.css

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


WRITTEN BY

Keeping a record