MERN-Stack Infrastructure - Part 6

Learning Objectives
Students Will Be Able To: |
---|
Use localStorage to Persist Data |
Better Understand How to Implement Features in the MERN-Stack |
Road Map
- The Plan - Part 6
- Infrastructure - Part 6 of 7
Videos
The Plan - Part 6
In Part 6 we will continue to implement user authentication.
Part 6 - Implementing Token-Based Auth (continued):
- Save the token in the browser's local storage
- Update the
user
state - Implement logging out
- Implement logging in
1. Save the Token in the Browser's Local Storage
In the previous lesson we created the JWT on the server when the visitor signed up. We also verified that the token was being sent back to the browser by logging it out in the console.
Because we will need to send the JWT to the server with any AJAX request that requires the controller action to know who the user is, we need to save the token in the client.
We can't simply assign the token to a variable or put it on state because a page refresh would loose the token.
Instead, we'll utilize the browser's localStorage to persist the JWT. This also enables the user to be logged in automatically when they browse to the app! That is, as long as the JWT hasn't expired.
❓ Where in the code does it make the most sense to persist the token to local storage?
The signUp
method in the users-service.js
module (when the token has been received from the server).
Here's the refactor:
export async function signUp(userData) {
...
const token = await usersAPI.signUp(userData);
// Persist the "token"
localStorage.setItem('token', token);
...
Note: Local Storage only stores and retrieves strings. When saving, the data will automatically be converted to a string, however, you will be responsible for using
JSON.parse()
to convert the string retrieved from local storage back into a number, boolean, array, object, etc.
Let's verify it's working by signing up again and checking out the Local Storage in DevTool's Application tab:

❓ What did we save in the JWT's payload when we created it?
The token's payload has a user
property that contains the data from the user's MongoDB document!
Time to put that payload to use...
2. Update the user
State
We need to set/update the user
state defined in the <App>
component whenever:
- The React app is loaded or refreshed.
- A visitor signs up.
- A user logs in.
- The user logs out.
Let's start with when the app is loaded/refreshed...
Setting the user
State When the Page is Loaded or Refreshed
❓ In plain language, what logic should we implement to set the user
state when the page loads/refreshes? Try to consider the three cases of token persistence in localStorage: Valid token exists; Expired token exists; and no token exists.
- Retrieve the token from localStorage.
- If there isn't a token, set
user
tonull
. - If there's an expired token, remove it from localStorage and set
user
tonull
. - If the token hasn't expired, extract the
user
object from the payload use set theuser
state to that object.
It makes sense to code much of the above logic in new getToken()
and getUser()
functions in users-service.js:
export function getToken() {
// getItem returns null if there's no string
const token = localStorage.getItem("token");
if (!token) return null;
// Obtain the payload of the token
const payload = JSON.parse(atob(token.split(".")[1]));
// A JWT's exp is expressed in seconds, not milliseconds, so convert
if (payload.exp < Date.now() / 1000) {
// Token has expired - remove it from localStorage
localStorage.removeItem("token");
return null;
}
return token;
}
export function getUser() {
const token = getToken();
// If there's a token, return the user in the payload, otherwise return null
return token ? JSON.parse(atob(token.split(".")[1])).user : null;
}
With those nifty functions in place, we can use getUser()
in <App>
to set the user
state.
First, import getUser
:
import { Routes, Route } from "react-router-dom";
// Add the following import
import { getUser } from "../../utilities/users-service";
Now let's put it to use with this tiny refactor:
export default function App() {
const [user, setUser] = useState(getUser());
We could use the React Developer Tools to verify it worked, but why not just add a bit of code to render the user's name in the <NavBar>
instead?
💪 Practice Exercise - Render the User's Name in <NavBar>
(5 minutes)
- Before
<NavBar>
can render the user's name, email, or whatever, you need to pass theuser
state as a prop (name the propuser
). - Render the user's name any way you wish in
<NavBar>
.
Hint:
<NavBar>
is currently not coded to accept any props

Setting the user
State When a Visitor Signs Up
Now we can finish the sign up functionality by updating the user
state after the visitor successfully signs up.
Currently, the signUp()
function in users-server.js is returning the token. However, if we take a look at the following code in SignUpForm.jsx...
// The promise returned by the signUp service method
// will resolve to the user object included in the
// payload of the JSON Web Token (JWT)
const user = await signUp(formData);
// Baby step!
console.log(user);
...we can see that we expect the signUp()
function to return the user object instead.
Nothing that a quick refactor in users-service.js can't handle:
export async function signUp(userData) {
try {
...
localStorage.setItem('token', token);
// Update the following line of code
return getUser();
...
Told you that would be a quick refactor 😊
We need a way to update user
state defined in <App>
from <SignUpForm>
. This requires that a function be passed from <App>
to <SignUpForm>
via a prop.
Ordinarily, if there's business/application logic that needs to be performed other than simply updating state, we would need to write a separate function and pass it via a prop. However, in this case, we simply need to update user
with the setUser()
setter function...
💪 Practice Exercise - Update user
State From <SignUpForm>
(5 minutes)
- Pass
setUser
from<App>
down the component hierarchy to<SignUpForm>
. - In
<SignUpForm>
, replace theconsole.log(user)
with a call to thesetUser
function, passing to ituser
.
Hints: Ordinarily we would need to destructure props passed to function components. However, class components like
<SignUpForm>
access their props asthis.props.<name of the prop>
so there's no destructuring or anything else necessary.
Let's use DevTools to manually clear the token from Local Storage, then sign up as a new user to test out the code!

Nice - congrats on implementing sign up functionality!
3. Implement Logging Out
AAU, I want to be able to log out of SEI CAFE just in case someone with the munchies gets a hold of my computer.
❓ What did we just do to effectively "log out" the currently logged in user?
Removed the token from local storage and set the user
state to null
.
You know the flow - start with the UI that the user is going to interact with.
Add Log Out UI
❓ Which component is the logical place to add a button or link used to log out?
<NavBar>
You already know how to use a <button>
with a click handler, but we can also use React Router's <Link>
if we prefer the "look" of a hyperlink vs. a button.
However, we don't want to use this particular <Link>
to navigate, so we'll leave its to
prop empty:
...
<nav>
...
<span>Welcome, {user.name}</span>
<Link to="">Log Out</Link>
</nav>
Clicking the rendered link will not navigate.
Add the onClick
Event Prop & Handler
Now let's add an onClick
prop and assign an event handler:
<Link to="" onClick={handleLogOut}>
Log Out
</Link>
Yup, we need to code that handleLogOut
handler:
export default function NavBar({ user }) {
// Add the following function
function handleLogOut() {
// Delegate to the users-service
userService.logOut();
// Update state will also cause a re-render
setUser(null);
}
...
Finish Implementing Log Out Functionality
❓ We're not done yet, based upon the code in the handler, what else do we need to do?
logOut
function in users-service.js.logOut
according to how we wrote the line of code that uses it.setUser
setter function as a prop to <NavBar>
.
Code the logOut
Function
All the logOut
function needs to do is remove the token:
export function logOut() {
localStorage.removeItem("token");
}
Import logOut
in <NavBar>
We're going to import using the syntax that matches the way we invoked the function:
// NavBar.jsx
import { Link } from "react-router-dom";
// Using the import below, we can call any exported function using: userService.someMethod()
import * as userService from "../../utilities/users-service";
Note: Using the above syntax to import provides some additional context when using the imported item.
Pass the setUser
Setter From <App>
to <NavBar>
💪 You got this!
Destructure the setUser
Prop in <NavBar>
💪 Slam dunk!
Log out and celebrate:

4. Implement Logging In
Logging in is very much like signing up!
First things first though, let's get the tedious stuff out of the way...
Create the <LoginForm>
Component
Productive developers always look to copy/paste work they've already written if it makes sense to do so.
❓ Is there a component that makes sense to copy/paste as the starting point for <LoginForm>
?
The <SignUpForm>
is a good candidate, but we would probably want to refactor it into a function component.
There's some good news and some bad news - which do you want first?
The Good News
The <LoginForm>
is below and ready to use!
import { useState } from "react";
import * as usersService from "../../utilities/users-service";
export default function LoginForm({ setUser }) {
const [credentials, setCredentials] = useState({
email: "",
password: "",
});
const [error, setError] = useState("");
function handleChange(evt) {
setCredentials({ ...credentials, [evt.target.name]: evt.target.value });
setError("");
}
async function handleSubmit(evt) {
// Prevent form from being submitted to the server
evt.preventDefault();
try {
// The promise returned by the signUp service method
// will resolve to the user object included in the
// payload of the JSON Web Token (JWT)
const user = await usersService.login(credentials);
setUser(user);
} catch {
setError("Log In Failed - Try Again");
}
}
return (
<div>
<div className="form-container">
<form autoComplete="off" onSubmit={handleSubmit}>
<label>Email</label>
<input
type="text"
name="email"
value={credentials.email}
onChange={handleChange}
required
/>
<label>Password</label>
<input
type="password"
name="password"
value={credentials.password}
onChange={handleChange}
required
/>
<button type="submit">LOG IN</button>
</form>
</div>
<p className="error-message"> {error}</p>
</div>
);
}
The Bad News
Just kidding - this is great news!
You've had considerable practice working with state, input, props, etc., so you're going to implement the rest of the login functionality as a group practice exercise!
💪 Practice Exercise - Implement Login Functionality (30 - 45 minutes)
Be sure to read all of the following before starting to code...
-
Add the
<LoginForm>
component above to the project following the naming convention for the folder and module. -
Render the
<LoginForm>
below the<SignUpForm>
in<AuthPage>
. It will be an icebox item to display only one of the forms at a time. -
Start implementing login functionality by reading the code in the
handleSubmit
function in LoginForm.jsx - that call tousersService.login(credentials)
starts your journey.
IMPORTANT: The existing code in LoginForm.jsx is complete - don't change anything.
-
Again, follow the flow from the UI to the server and back.
-
Use the code and logic we used to implement Sign Up functionality as a guide. The
login
functions that need to be added to the users-service.js and users-api.js modules are similar to the existingsignUp
functions. -
FYI, the solution code uses the server-side route of
POST /api/users/login
mapped to a controller action namedlogin
. -
The
login
controller action is the most challenging. Although in structure it's similar tocreate
, it has slightly different functionality - instead of creating the user we need to query for the user based upon theiremail
and then verify the password is correct using bcrypt'scompare
method.Hint 1: The
User
model'sfindOne
is the appropriate query method to use to find a user based on their email.Hint 2: Remember to require the bcrypt library.
Hint 3: When invoking bcrypt's
compare
method, use the syntax that returns a promise and consume it withawait
.
Peek if you must...
const match = await bcrypt.compare(req.body.password, user.password);
Hint 4: Be sure to structure the code so that it responds with a status code of 400 if either the user is not found in the database (bad email) or if the user is found but the password doesn't match.
Feel free to use the following code if you get stuck or run out of time
// Be Sure to add the following
const bcrypt = require("bcrypt");
module.exports = {
create,
login,
};
async function login(req, res) {
try {
const user = await User.findOne({ email: req.body.email });
if (!user) throw new Error();
const match = await bcrypt.compare(req.body.password, user.password);
if (!match) throw new Error();
res.json(createJWT(user));
} catch {
res.status(400).json("Bad Credentials");
}
}
- See how far you can get and feel free to reach out for assistance if you get stuck - enjoy!
Icebox
- Instead of showing both the
<SignUpForm>
and<LoginForm>
simultaneously, implement showing one or the other in<AuthPage>
- just like the deployed SEI CAFE does.
Hint: This is an obvious use case for a piece of ui-related state.