Build a Login/Auth App with the MERN Stack — Part 2 (Frontend & Redux Setup)
Create a (minimal) full-stack app with user authentication via passport and JWTs
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 mLab
- Set up a server with
Node.js
andExpress
- Created a database schema to define a
User
for registration and login purposes - Set up two API routes,
register
andlogin
, usingpassport
+jsonwebtoken
s for authentication andvalidator
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
with create-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/rishi/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 our client
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 using npm
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 backendclassnames
: used for conditional classes in ourJSX
jwt-decode
: used to decode ourjwt
so we can get user data from itreact-redux
: allows us to useRedux
withReact
react-router-dom
: used for routing purposesredux
: used to manage state between components (can be used withReact
or any other view library)redux-thunk
: middleware forRedux
that allows us to directly access thedispatch
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": {
"axios": "^0.18.0",
"classnames": "^2.2.6",
"jwt-decode": "^2.2.0",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"react-redux": "^5.1.1",
"react-router-dom": "^4.3.1",
"react-scripts": "2.1.1",
"redux": "^4.0.1",
"redux-thunk": "^2.3.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"proxy": "http://localhost:5000",
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
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
inclient/src
- Take out the
import
oflogo.svg
inApp.js
- Remove all the CSS in
App.css
(we’ll keep theimport
inApp.js
in case you want to add your own global CSS here) - Clear out the content in the main
div
inApp.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.
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
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 fromreact-router-dom
rather than standard<a>
tags (note theto:
in<Link>
versus a typicalhref=
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()
.
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 anonChange
event that ties its value to our components state - In our
onSubmit
event, we’ll usee.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
.
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 Route
s 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
andreducers
,
In reducers
,
- Create
index.js
,authReducers.js
, anderrorReducers.js
files
In actions
,
- Create
authActions.js
andtypes.js
files
For convenience, you can run the following within src
.
➜ src touch store.js && mkdir actions reducers && cd reducers && touch index.js authReducers.js errorReducers.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 ourtypes.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 makeHTTPRequests
within certain action - Use
dispatch
to send actions to ourreducers
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
into 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.
Also, please feel free to comment, ask anything or suggest your ideas!