[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-end
Current 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-app
creates.git-ignore
file infront-end
directory. If you plan to push anything on a repository, move.git-ignore
out offront-end
and place it undermern-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
.
"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 insidemern-demo/front-end/package.json
. This is equivalent tocd front-end && npm i
npm run client
is equivalent to runningcd front-end && npm start
npm run dev
this 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:8080
front-end/.env.production
REACT_APP_FETCH_URL=<YOUR_URL_ON_PRODUCTION>
React will use variables in
.env
on development, but onnpm 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.
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 User
custom-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 CustomButton
user-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 UserAddForm
App.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 App
App.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