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

Rishi
Bits and Pieces

--

The finished product: a full-stack MERN app with Redux for state management (repo)

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 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 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 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": {
"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 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.

<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 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().

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.

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.

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.

Helpful React-Redux data flow diagram from @nikgraf

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, authReducers.js, and errorReducers.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 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 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 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!

--

--