Before we get started
Read Part 1: Creating our backend
In Part 1, we
Initialized our backend using npm and installed necessary packages
Set up a MongoDB database using cloud mongodb
Set up a server with Node.js and Express
Created a database schema to define a User for registration and login purposes
Set up two API routes, register and login, using passport + jsonwebtokens for authentication and validator for input validation
Tested our API routes using Postman
In this part, we will
Set up our frontend using create-react-app
Create static components for our Navbar, Landing, Login and Register pages
Setup Redux for global state management
Install the React and Redux Chrome Extensions:
Please note that this is
not
a
Redux tutorial
. I try my best to explain things as I go along, but there are better resources to learn how to use Redux with React.
Part 2: Creating our frontend & setting up Redux
i. Setting up our frontend
1. Edit the root
package.json
Edit the "scripts" object to the following in our server’s package.json.
"scripts": {
"client-install": "npm install --prefix client",
"start": "node server.js",
"server": "nodemon server.js",
"client": "npm start --prefix client",
"dev": "concurrently \"npm run server\" \"npm run client\""
},
We’ll use concurrently to run both our backend and frontend (client) at the same time. We’ll use
npm run dev
to run this command later on.
2. Scaffold our
client
withcreate-react-app
We’ll be using
create-react-app
to set up our client. This will take care of a lot of heavy lifting for us (as opposed to creating a React project from scratch). I use this command to start all my React projects.
First, if you haven’t installed it already, run npm i -g create-react-app to install create-react-app globally.
Now, create a client directory and run create-react-app within it.
➜ mern-auth mkdir client && cd client && create-react-app .
Creating a new React app in /Users/PatrickBiyaga/mern-auth/client.
Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts...
3. Change our
package.json
within ourclient
directory
When we make requests from React with axios, we don’t want to have to do the following in our requests:
axios.post(‘http://localhost:5000/api/users/register');
We want to be able to do the following instead.
axios.post('/api/users/register');
To achieve this, add the following under the "scripts" object in our client's package.json.
"proxy": "http://localhost:5000",
4. Within
client
, install the following dependencies usingnpm
npm i axios classnames jwt-decode react-redux react-router-dom redux redux-thunk
A brief description of each package and the function it will serve
axios
: promise based HTTP client for making requests to our backend
classnames
: used for conditional classes in our JSX
jwt-decode
: used to decode our jwt so we can get user data from it
react-redux
: allows us to use Redux with React
react-router-dom
: used for routing purposes
redux
: used to manage state between components (can be used with React or any other view library)
redux-thunk
: middleware for Redux that allows us to directly access the dispatch method to make asynchronous calls from our actions
Your
client‘s
package.json should look something like this.
{
"name": "client",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"axios": "^0.21.1",
"classnames": "^2.2.6",
"jwt-decode": "^3.1.2",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.1",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"web-vitals": "^0.2.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://localhost:5000"
}
5. Run
npm run dev
and test if bothserver
andclient
run concurrently and successfully
5. Run npm run dev and test if both server and client run concurrently and successfully
Navigate to localhost:3000 to view your React app. Depending on what version of create-react-app you are running, it may look a bit different.
6. Clean up our React app by removing unnecessary files and code
Remove logo.svg in client/src
Take out the import of logo.svg in App.js
Remove all the CSS in App.css (we’ll keep the import in App.js in case you want to add your own global CSS here)
Clear out the content in the main div in App.js and replace it with an <h1> for now
You should have no errors and your App.js should look like this at this point.
import React, { Component } from "react";
import "./App.css";
class App extends Component {
render() {
return (
<div className="App">
<h1>Hello</h1>
</div>
);
}
}
export default App;
Navigate back to localhost:3000 and you should see this.
7. Install
Materialize.css
by editing our index.html in client/public
Navigate to the CDN portion and grab the CSS and Javascript tags.
In
client/public/index.html
, add the CSS tag above the <head> tag and the JS script right above the </body> tag. Let’s change the <title> from “React App” to the name of your app while we’re here as well (this is what shows in the toolbar when the app is running).
Let’s also add the following CSS tag under our Materialize tag for access to Google’s Material Icons.
Your index.html should now look like this.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<!-- Compiled and minified CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<title>MERN Auth App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
</body>
</html>
ii. Creating our static components
In our src
directory, let’s create a folder for our components
and create a layout
folder within it to host our “layout” components shared throughout the app (e.g. Landing page, Navbar).
➜ src mkdir components && cd components && mkdir layout && cd layout && touch Navbar.js Landing.js
1. Common Components: Navbar & Landing
We will be using the <Link> tag from react-router-dom rather than standard <a> tags (note the to: in <Link> versus a typical href= in an <a> tag)
Let’s put the following in our Navbar.js.
import React, { Component } from "react";
import { Link } from "react-router-dom";
class Navbar extends Component {
render() {
return (
<div className="navbar-fixed">
<nav className="z-depth-0">
<div className="nav-wrapper white">
<Link
to="/"
style={{
fontFamily: "monospace"
}}
className="col s5 brand-logo center black-text"
>
<i className="material-icons">code</i>
MERN
</Link>
</div>
</nav>
</div>
);
}
}
export default Navbar;
For our Landing
component, we’ll just have some basic HTML/CSS
to create our page (technically JSX
and not HTML
).
Let’s put the following in our Landing.js.
import React, { Component } from "react";
import { Link } from "react-router-dom";
class Landing extends Component {
render() {
return (
<div style={{ height: "75vh" }} className="container valign-wrapper">
<div className="row">
<div className="col s12 center-align">
<h4>
<b>Build</b> a login/auth app with the{" "}
<span style={{ fontFamily: "monospace" }}>MERN</span> stack from
scratch
</h4>
<p className="flow-text grey-text text-darken-1">
Create a (minimal) full-stack app with user authentication via
passport and JWTs
</p>
<br />
<div className="col s6">
<Link
to="/register"
style={{
width: "140px",
borderRadius: "3px",
letterSpacing: "1.5px"
}}
className="btn btn-large waves-effect waves-light hoverable blue accent-3"
>
Register
</Link>
</div>
<div className="col s6">
<Link
to="/login"
style={{
width: "140px",
borderRadius: "3px",
letterSpacing: "1.5px"
}}
className="btn btn-large btn-flat waves-effect white black-text"
>
Log In
</Link>
</div>
</div>
</div>
</div>
);
}
}
export default Landing;
Finally, let’s import our Navbar and Landing components into our App.js file and add them to our render().
file and add them to our render()
.
import React, { Component } from "react";
import "./App.css";
import Navbar from "./components/layout/Navbar";
import Landing from "./components/layout/Landing";
class App extends Component {
render() {
return (
<div className="App">
<Navbar />
<Landing />
</div>
);
}
}
export default App;
2. Auth Components: Register and Login
Now, let’s create a directory for our auth components and create Login.js and Register.js files within it.
➜ components mkdir auth && cd auth && touch Login.js Register.js
Forms work a bit differently in React. It may be helpful to first read the documentation on
forms
and on
handling events
in React.
Every form element has an onChange event that ties its value to our components state
In our onSubmit event, we’ll use e.preventDefault() to stop the page from reloading when the submit button is clicked
A side note on destructuring in React: const { errors } = this.state; is the same as doing const errors = this.state.errors;. It is less verbose and looks cleaner in my opinion.
Let’s place the following in our Register.js.
import React, { Component } from "react";
import { Link } from "react-router-dom";
class Register extends Component {
constructor() {
super();
this.state = {
name: "",
email: "",
password: "",
password2: "",
errors: {}
};
}
onChange = e => {
this.setState({ [e.target.id]: e.target.value });
};
onSubmit = e => {
e.preventDefault();
const newUser = {
name: this.state.name,
email: this.state.email,
password: this.state.password,
password2: this.state.password2
};
console.log(newUser);
};
render() {
const { errors } = this.state;
return (
<div className="container">
<div className="row">
<div className="col s8 offset-s2">
<Link to="/" className="btn-flat waves-effect">
<i className="material-icons left">keyboard_backspace</i> Back to
home
</Link>
<div className="col s12" style={{ paddingLeft: "11.250px" }}>
<h4>
<b>Register</b> below
</h4>
<p className="grey-text text-darken-1">
Already have an account? <Link to="/login">Log in</Link>
</p>
</div>
<form noValidate onSubmit={this.onSubmit}>
<div className="input-field col s12">
<input
onChange={this.onChange}
value={this.state.name}
error={errors.name}
id="name"
type="text"
/>
<label htmlFor="name">Name</label>
</div>
<div className="input-field col s12">
<input
onChange={this.onChange}
value={this.state.email}
error={errors.email}
id="email"
type="email"
/>
<label htmlFor="email">Email</label>
</div>
<div className="input-field col s12">
<input
onChange={this.onChange}
value={this.state.password}
error={errors.password}
id="password"
type="password"
/>
<label htmlFor="password">Password</label>
</div>
<div className="input-field col s12">
<input
onChange={this.onChange}
value={this.state.password2}
error={errors.password2}
id="password2"
type="password"
/>
<label htmlFor="password2">Confirm Password</label>
</div>
<div className="col s12" style={{ paddingLeft: "11.250px" }}>
<button
style={{
width: "150px",
borderRadius: "3px",
letterSpacing: "1.5px",
marginTop: "1rem"
}}
type="submit"
className="btn btn-large waves-effect waves-light hoverable blue accent-3"
>
Sign up
</button>
</div>
</form>
</div>
</div>
</div>
);
}
}
export default Register;
Our Login component will look similar to our Register component. Let’s place the following in our Login.js.
place the following in our Login.js
.
import React, { Component } from "react";
import { Link } from "react-router-dom";
class Login extends Component {
constructor() {
super();
this.state = {
email: "",
password: "",
errors: {}
};
}
onChange = e => {
this.setState({ [e.target.id]: e.target.value });
};
onSubmit = e => {
e.preventDefault();
const userData = {
email: this.state.email,
password: this.state.password
};
console.log(userData);
};
render() {
const { errors } = this.state;
return (
<div className="container">
<div style={{ marginTop: "4rem" }} className="row">
<div className="col s8 offset-s2">
<Link to="/" className="btn-flat waves-effect">
<i className="material-icons left">keyboard_backspace</i> Back to
home
</Link>
<div className="col s12" style={{ paddingLeft: "11.250px" }}>
<h4>
<b>Login</b> below
</h4>
<p className="grey-text text-darken-1">
Don't have an account? <Link to="/register">Register</Link>
</p>
</div>
<form noValidate onSubmit={this.onSubmit}>
<div className="input-field col s12">
<input
onChange={this.onChange}
value={this.state.email}
error={errors.email}
id="email"
type="email"
/>
<label htmlFor="email">Email</label>
</div>
<div className="input-field col s12">
<input
onChange={this.onChange}
value={this.state.password}
error={errors.password}
id="password"
type="password"
/>
<label htmlFor="password">Password</label>
</div>
<div className="col s12" style={{ paddingLeft: "11.250px" }}>
<button
style={{
width: "150px",
borderRadius: "3px",
letterSpacing: "1.5px",
marginTop: "1rem"
}}
type="submit"
className="btn btn-large waves-effect waves-light hoverable blue accent-3"
>
Login
</button>
</div>
</form>
</div>
</div>
</div>
);
}
}
export default Login;
Our components won’t display until we define our login and register routes in our App.js using react-router-dom.
3. Setting up React Router in our
App.js
We’ll define our routing paths using react-router-dom
.
<Route exact path=”/register” component={Register} /> means at localhost:3000/register, render the Register component.
Add the following to your App.js. Make sure to wrap the <div className="App"> tag with a starting and closing Router tag. Let’s also pull in our Register and Login components and create Routes for each of them.
in our Register and Login components and create Routes for each of them.
import React, { Component } from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
import Navbar from "./components/layout/Navbar";
import Landing from "./components/layout/Landing";
import Register from "./components/auth/Register";
import Login from "./components/auth/Login";
class App extends Component {
render() {
return (
<Router>
<div className="App">
<Navbar />
<Route exact path="/" component={Landing} />
<Route exact path="/register" component={Register} />
<Route exact path="/login" component={Login} />
</div>
</Router>
);
}
}
export default App;
Your app should look something like this now (assuming you haven’t changed the above code). Notice the console.log() statements as we submit the forms (defined in our form’s onSubmit event). We haven’t linked up our frontend with our backend yet, so we’re not actually registering or logging in users, but we’ll be sending these objects to Redux (and in turn, our backend) to complete those actions.
iii. Setting up Redux for state management
This is not meant to be a Redux tutorial, but I’ll explain it a bit as I go along (actions, reducers, store). This
video
by TraversyMedia (also linked at top of post) does a great job explaining React+Redux—I would watch it if this is your first time working with Redux.
While in this form, our app doesn’t
require
Redux at all, this series is meant to be a base to build off for a more functional, larger-scale MERN app. The development community largely agrees that Redux is pretty much necessary for any large-scale applications, as managing state between many React components would likely turn out to be a nightmare. Instead of passing state from component to component, Redux provides a single source of truth that you can dispatch to any of your components.
In my opinion, the hardest (or most annoying) part of implementing Redux is all of the boilerplate setup. However, once we have Redux setup, defining more actions or types becomes easy, and the
flow of data in the app is intuitive
.
1. Make the following bolded additions to App.js
Make sure to wrap your entire return statement with a <Provider store={store}> tag (and closing tag).
import React, { Component } from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
import { Provider } from "react-redux";
import store from "./store";
import Navbar from "./components/layout/Navbar";
import Landing from "./components/layout/Landing";
import Register from "./components/auth/Register";
import Login from "./components/auth/Login";
class App extends Component {
render() {
return (
<Provider store={store}>
<Router>
<div className="App">
<Navbar />
<Route exact path="/" component={Landing} />
<Route exact path="/register" component={Register} />
<Route exact path="/login" component={Login} />
</div>
</Router>
</Provider>
);
}
}
export default App;
We haven’t defined our store yet (so the above will throw an error), but we’ll do that shortly.
2. Setting up our Redux file structure
In src
,
Create a store.js file
Create directories for actions and reducers,
In
reducers
,
Create index.js, authReducer.js, and errorReducer.js files
In
actions
,
Create authActions.js and types.js files
For convenience, you can run the following within src.
➜ src touch store.js && mkdir actions reducers && cd reducers && touch index.js authReducer.js errorReducer.js && cd ../ && cd actions && touch authActions.js types.js
3. Setting up our
store
createStore() creates a Redux
store
that holds the complete state tree of your app. There should only be a single store in your app.
Our store also sends application state to our React components, which will react accordingly to that state.
Place the following in store.js. We’ll pass an empty rootReducer for now as the first parameter to createStore() since we haven’t created our reducers yet.
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
const initialState = {};
const middleware = [thunk];
const store = createStore(
() => [],
initialState,
compose(
applyMiddleware(...middleware),
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)
);
export default store;
4. Defining our
actions
An interaction (such as a button click or a form submission) in our React components will fire off an action and, in turn, dispatch an action to our store.
In our actions folder, let’s place the following in types.js.
export const GET_ERRORS = "GET_ERRORS";
export const USER_LOADING = "USER_LOADING";
export const SET_CURRENT_USER = "SET_CURRENT_USER";
5. Creating our
reducers
Reducers are pure functions that specify how application state should change in response to an action. Reducers respond with the new state, which is passed to our store and, in turn, our UI.
Our flow for reducers will go as follows.
Import all our actions from our types.js file
Define our initialState
Define how state should change based on actions with a switch statement
i. Creating our authReducer.js
Let’s place the following in our authReducer.js.
import {
SET_CURRENT_USER,
USER_LOADING
} from "../actions/types";
const isEmpty = require("is-empty");
const initialState = {
isAuthenticated: false,
user: {},
loading: false
};
export default function(state = initialState, action) {
switch (action.type) {
case SET_CURRENT_USER:
return {
...state,
isAuthenticated: !isEmpty(action.payload),
user: action.payload
};
case USER_LOADING:
return {
...state,
loading: true
};
default:
return state;
}
}
ii. Creating our errorReducer.js
Let’s place the following in our errorReducer.js.
import { GET_ERRORS } from "../actions/types";
const initialState = {};
export default function(state = initialState, action) {
switch (action.type) {
case GET_ERRORS:
return action.payload;
default:
return state;
}
}
iii. Creating our rootReducer
in index.js
We’ll use combinedReducers from redux to combine our authReducer and errorReducer into one rootReducer.
Let’s define our rootReducer
by adding the following to our index.js
.
import { combineReducers } from "redux";
import authReducer from "./authReducer";
import errorReducer from "./errorReducer";
export default combineReducers({
auth: authReducer,
errors: errorReducer
});
6. Setting our
auth
token
Before we begin creating our actions
, let’s create a utils
directory within src
, and within it, a setAuthToken.js
file.
➜ src mkdir utils && cd utils && touch setAuthToken.js
We’ll use this to set and delete the Authorization
header for our axios
requests depending on whether a user is logged in or not (remember in Part 1 how we set an Authorization
header in Postman when testing our private api
route?).
Let’s place the following in setAuthToken.js
.
import axios from "axios";
const setAuthToken = token => {
if (token) {
// Apply authorization token to every request if logged in
axios.defaults.headers.common["Authorization"] = token;
} else {
// Delete auth header
delete axios.defaults.headers.common["Authorization"];
}
};
export default setAuthToken;
7. Creating our
actions
Our general flow for our actions will be as follows.
Import dependencies and action definitions from types.js
Use axios to make HTTPRequests within certain action
Use dispatch to send actions to our reducers
Let’s place the following in authActions.js
.
import axios from "axios";
import setAuthToken from "../utils/setAuthToken";
import jwt_decode from "jwt-decode";
import {
GET_ERRORS,
SET_CURRENT_USER,
USER_LOADING
} from "./types";
// Register User
export const registerUser = (userData, history) => dispatch => {
axios
.post("/api/users/register", userData)
.then(res => history.push("/login")) // re-direct to login on successful register
.catch(err =>
dispatch({
type: GET_ERRORS,
payload: err.response.data
})
);
};
// Login - get user token
export const loginUser = userData => dispatch => {
axios
.post("/api/users/login", userData)
.then(res => {
// Save to localStorage
// Set token to localStorage
const { token } = res.data;
localStorage.setItem("jwtToken", token);
// Set token to Auth header
setAuthToken(token);
// Decode token to get user data
const decoded = jwt_decode(token);
// Set current user
dispatch(setCurrentUser(decoded));
})
.catch(err =>
dispatch({
type: GET_ERRORS,
payload: err.response.data
})
);
};
// Set logged in user
export const setCurrentUser = decoded => {
return {
type: SET_CURRENT_USER,
payload: decoded
};
};
// User loading
export const setUserLoading = () => {
return {
type: USER_LOADING
};
};
// Log user out
export const logoutUser = () => dispatch => {
// Remove token from local storage
localStorage.removeItem("jwtToken");
// Remove auth header for future requests
setAuthToken(false);
// Set current user to empty object {} which will set isAuthenticated to false
dispatch(setCurrentUser({}));
};
8. Pulling our
rootReducer
intostore.js
Make the following
bolded
additions to store.js.
Make the following bolded additions to store.js
.
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import rootReducer from "./reducers";
const initialState = {};
const middleware = [thunk];
const store = createStore(
rootReducer,
initialState,
compose(
applyMiddleware(...middleware),
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)
);
export default store;
And that’s it for our frontend and redux setup!
We’ve successfully set up our frontend and Redux for state management. Give yourself a pat on the back for following along. Throw some claps too. 👏
In
Part 3
below (last post in this series), we’ll link Redux with our components and use axios to fetch data from our server.