We have two more files to touch before completing our backend. Before we create our session routes, we’re going to define two helper methods in util/helpers.js:

--- backend/util/helpers.js ---

export const parseError = err => {

if (err.isJoi) return err.details[0];

return JSON.stringify(err, Object.getOwnPropertyNames(err));

}; export const sessionizeUser = user => {

return { userId: user.id, username: user.username };

}

The first function will allow us to differentiate Joi errors from custom Errors we throw later and extract the message, which we’ll send to the client and display to our users.

The second returns an object containing exactly what we want to save to our session and send back to our Redux store later.

Let’s add our error parser to routes/user.js and save our user to our session:

--- backend/routes/user.js ---

import Joi from 'joi';

import express from 'express';

import User from '../models/user';

import { signUp } from '../validations/user';

import { parseError, sessionizeUser } from "../util/helpers"; const userRouter = express.Router();

userRouter.post("", async (req, res) => {

try {

const { username, email, password } = req.body;

await Joi.validate({ username, email, password }, signUp); const newUser = new User({ username, email, password });

const sessionUser = sessionizeUser(newUser);

await newUser.save(); req.session.user = sessionUser;

res.send(sessionUser);

} catch (err) {

res.status(400).send(parseError(err));

}

}); export default userRouter;

After setting up express-session, we have access to the session object, which is only saved server-side (the client does/will not have access to this session). With that in mind, we save our user’s id and username into our session.

To show it works, I put a console.log(req.session) right under where we stored our user. I then created another user using Postman, and there he is!

Alight, now let’s create a session.js in our routes/ folder:

--- backend/routes/session.js ---

import express from "express";

import Joi from "joi";

import User from "../models/user";

import { signIn } from "../validations/user";

import { parseError, sessionizeUser } from "../util/helpers";

import { SESS_NAME } from "../config"; const sessionRouter = express.Router();

...

First, we import everything we need. No surprises here except maybe SESS_NAME, you’ll see soon enough why we need that.

We then define sessionRouter the same we defined our userRouter.

Now we’re going to define three API endpoints starting with our login:

--- backend/routes/session.js ---

...

sessionRouter.post("", async (req, res) => {

try {

const { email, password } = req.body

await Joi.validate({ email, password }, signIn); const user = await User.findOne({ email });

if (user && user.comparePasswords(password)) {

const sessionUser = sessionizeUser(user); req.session.user = sessionUser

res.send(sessionUser);

} else {

throw new Error('Invalid login credentials');

}

} catch (err) {

res.status(401).send(parseError(err));

}

});

...

Let’s talk about this. We define a post method on our sessionRouter, we add async, pull email and password from the request’s body, and use our signIn Joi validator.

If that passes, we query our user collection using the findOne mongoose method, making sure to use await. We then check if we got a user and if the supplied password matches the hashed password (The order of logic here is important).

If that’s a success, we sessionize our user, store their data in the session object, and send their info to the client. Otherwise, we catch any error, be it a Joi error or the one we throw when we say new Error(‘Invalid login credentials’) , set a failing status code, and send back the parsed error.

Next in line: Logout.

--- backend/routes/session.js ---

...

sessionRouter.delete("", ({ session }, res) => {

try {

const user = session.user;

if (user) {

session.destroy(err => {

if (err) throw (err); res.clearCookie(SESS_NAME);

res.send(user);

});

} else {

throw new Error('Something went wrong');

}

} catch (err) {

res.status(422).send(parseError(err));

}

});

...

Here we destructure session out of the request object (since we don’t need anything else) and pull the user out of that session. If there is one, we call the built-in destroy method.

Then we call the clearCookie(session_name) method on our response and send back the user to the client.

Our last endpoint simply checks if a user is logged in or not. This one’s simple:

--- backend/routes/session.js ---

...

sessionRouter.get("", ({ session: { user }}, res) => {

res.send({ user });

}); export default sessionRouter;

We destructure our request down to the user. This will either be our user or undefined. In any case, we send back an object of user pointing to whatever we get out of our session.

There are only two more tasks we must complete! In routes/index.js:

--- backend/routes/index.js ---

import userRoutes from './user';

import sessionRoutes from './session'; export { userRoutes, sessionRoutes };

Annnnd in our server.js:

--- backend/server.js ---

...

const apiRouter = express.Router();

app.use('/api', apiRouter);

apiRouter.use('/users', userRoutes);

apiRouter.use('/session', sessionRoutes);

...

That’s it! Our backend is now complete! 🎉

Give yourself a pat on the back. You just wrote a backend from scratch! 👍

Step 6: React/Redux Setup

Before continuing, if this is your first time using React and Redux, these concepts might sound a little confusing, but don’t worry! They’re very repetitive and just take practice. As always, I’ll explain things at a high level but if your head is spinning I highly recommend the React and Redux documentation. They’re fantastic! Lastly, mind the implicit returns. We’ll be using a lot of them. Ready?

Let’s change directories up one level to our root project directory, SessionAuth: cd ..

Now run this command: npx create-react-app frontend

This sets up a React application without any build configuration. This creates quite a bit for us, so we’ll need to do a little house-cleaning.

Feel free to keep any files you want/need. I’m going bare-bones for this app so it can be re-used for future applications without any baggage. I deleted: public/favicon.ico, manifest.json, src/App.css, App.test.js, index.css, logo.svg, serviceWorker.js and rearranged my file structure like so:

SessionAuth/

|— backend/

|— frontend/

|— node_modules/

|— public/

|— index.html

|— src/

|— actions/

|— components/

|— App.js <= moved

|— reducers/

|— errors/

|— session/

|— store/

|— util/

|— index.js

|— .gitignore

|— package-lock.json

|— package.json

I gave the index.html a trim as well:

--- frontend/public/index.html ---

<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="utf-8" />

<meta

name="viewport"

content="width=device-width, initial-scale=1, shrink-to-fit=no"

/>

<title>Session Auth</title>

</head>

<body>

<noscript>You need to enable JavaScript to run this app.</noscript>

<div id="root"></div>

</body>

</html>

Next stop is our App.js:

--- frontend/src/components/App.js ---

import React from 'react'; export default () => (

<>

<h1>Hello world</h1>

</>

);

At a very high-level, components are just functions that return JSX, which produces React elements, which are then rendered to the DOM.

To keep things simple we won’t be using any stateful components.

We will be using fragments, which allow us to group elements without adding extra nodes to the DOM. Here we use one to house our h1. We’ll come back here later.

Now for our index.js:

--- frontend/src/index.js ---

import React from 'react';

import ReactDOM from 'react-dom';

import App from './components/App';

// delete old imports we don't need ReactDOM.render(

<App />,

document.getElementById('root')

);

We will be adding a lot more here later, but for now, this is good. We are rendering the App component inside the <div id=”root”></div> from our index.html, which in turn renders that h1.



Remember when we saved port 3000 for our client? Well, we still need a way for our client to communicate with our backend. For this, we will use a proxy.

In our frontend package.json at the bottom we add:

--- frontend/package.json ---

...

"browserslist": [

">0.2%",

"not dead",

"not ie <= 11",

"not op_mini all"

],

"proxy": "http://localhost:5000"

}

This tells our server to use our backend resources when we make a request to something like “/api/session”.

Now, in a separate terminal tab or window, keeping the backend server running, change directories into our frontend/ folder: cd frontend/ , and run this command: npm start

Now go to http://localhost:3000/ and you should see the “Hello world”.

Now in another separate terminal tab or window (you should have a minimum of three open now), making sure you are in the frontend/ directory and install these dependencies: npm i react-redux react-router-dom redux redux-thunk

Alright. Now everything is in place for us to start integrating Redux!

Let’s begin by creating our HTTP request functions in util/session.js:

--- frontend/util/session.js ---

export const signup = user => (

fetch("api/users", {

method: "POST",

body: JSON.stringify(user),

headers: {

"Content-Type": "application/json"

}

})

); export const login = user => (

fetch("api/session", {

method: "POST",

body: JSON.stringify(user),

headers: {

"Content-Type": "application/json"

}

})

); export const logout = () => (

fetch("api/session", { method: "DELETE" })

);

Here we are using the Fetch API to access three of our backend endpoints. Remember when using Postman we selected “application/json”? We want to do the same thing here, but we make sure the body of our request is in a json format by using JSON.stringify().

Before we move any further, let’s talk about how we want our frontend state to be shaped—referring to our Redux store.

When thinking about state-shape it’s good to ask these two questions:

1. What data does our application need access to at any given moment?

2. How can we organize this data in a way that is normalized?

Since this app will only consist of keeping track of a session and signup/login errors, here’s what I have in mind for now:

{

session: { id, username },

errors: ""

}

We’re keeping it very simple.

Let’s set up our actions now. Create a session.js in actions/:

--- frontend/actions/session.js ---

import * as apiUtil from ‘../util/session’; export const RECEIVE_CURRENT_USER = ‘RECEIVE_CURRENT_USER’;

export const LOGOUT_CURRENT_USER = ‘LOGOUT_CURRENT_USER’; const receiveCurrentUser = user => ({

type: RECEIVE_CURRENT_USER,

user

}); const logoutCurrentUser = () => ({

type: LOGOUT_CURRENT_USER

});

...

Here we import our fetch functions as apiUtil, so we don’t have any namespace conflicts when using signup/login/logout.

Next, we define two action types which will help our reducers decide what to do with our actions. Using string constants and exporting them ensures our reducers don’t let a typo slide by silently. Also, when working with larger applications, all of these can be abstracted away into an actionTypes.js file.

After that, we define two actions. Actions are JavaScript objects that supply our Redux store with payloads of data (in this case from our backend).

Later, we will be using middleware which decides to either make more API calls or move to the next step—reducers—based on on the premise if an object is a function or a POJO (plain ol’ JavaScript object).

For this reason, we will define action creators, which our components will dispatch to gather resources from the backend.

After those requests are made, these action creators will then dispatch our actions defined above. The reducers will then filter the data and assign its place in our Redux store.

Alrighty then… Let’s define these action creators:

--- frontend/actions/session.js ---

...

export const login = user => async dispatch => {

const response = await apiUtil.login(user);

const data = await response.json(); if (response.ok) {

return dispatch(receiveCurrentUser(data));

}

// todo: handle errors

}; export const signup = user => async dispatch => {

const response = await apiUtil.signup(user);

const data = await response.json();



if (response.ok) {

return dispatch(receiveCurrentUser(data));

}

// todo: handle errors

}; export const logout = () => async dispatch => {

const response = await apiUtil.logout();

const data = await response.json(); if (response.ok) {

return dispatch(logoutCurrentUser());

}

// todo: handle errors

};

All three functions follow the same logic: Return an asynchronous function that takes in dispatch as an argument, make an HTTP request, and extract the data from the response.

If the response is OK (200-level), dispatch an action we defined above with the payload. If the response is not OK, then we handle the error. It helps to visualize it like this:

export const login = function(user) {

return async function(dispatch) {

...

};

};

But since we’re JavaScript pros, we’ll keep it ESNext.

Let’s quickly take care of a way to handle those errors. Create an error.js in our actions/ folder:

--- frontend/actions/error.js ---

export const RECEIVE_ERRORS = 'RECEIVE_ERRORS';

export const CLEAR_ERRORS = 'CLEAR_ERRORS'; export const receiveErrors = ({ message }) => ({

type: RECEIVE_ERRORS,

message

}); export const clearErrors = () => ({

type: CLEAR_ERRORS

});

Same pattern. Now, back in our session.js:

--- frontend/actions/session.js ---

import * as apiUtil from ‘../util/session’;

import { receiveErrors } from "./error";

...

if (response.ok) {

return dispatch(receiveCurrentUser(data));

}

return dispatch(receiveErrors(data));

};

...

if (response.ok) {

return dispatch(receiveCurrentUser(data));

}

return dispatch(receiveErrors(data));

};

...

if (response.ok) {

return dispatch(logoutCurrentUser());

}

return dispatch(receiveErrors(data));

};

Our actions are done! 💪

Let’s get started on our reducers! In reducers/root.js:

--- frontend/reducers/root.js ---

import { combineReducers } from 'redux';

import errors from './errors/errors';

import session from './session/session'; export default combineReducers({

session,

errors

});

We have not created our error and session reducers yet, but combineReducers does exactly what we’d expect here. It combines our reducers, putting them at the same depth-level in our frontend state. These can be nested even further, albeit a state too deeply nested presents a code-smell. You’ll often see an entities reducer here, and the entities reducer will then combine more reducers like users, posts, etc…

Now let’s create those two reducers. Let’s create another session.js file inside reducers/:

--- frontend/reducers/session/session.js ---

import {

RECEIVE_CURRENT_USER,

LOGOUT_CURRENT_USER

} from "../../actions/session"; const _nullSession = { userId: null, username: null }

export default (state = _nullSession, { type, user }) => {

Object.freeze(state);

switch (type) {

case RECEIVE_CURRENT_USER:

return user;

case LOGOUT_CURRENT_USER:

return _nullSession;

default:

return state;

}

};

Let’s discuss this a little. The reducer is a function that takes in two arguments—the current state and the action. There are three things I want to point out about reducers:

1. The state that they take in is not the entire state. For example, this session reducer cannot affect the state shape of our errors or any other part of the state. Only the session.

2. Every action hits every reducer. That is why all actions have a type property and why reducers use switch statements.

3. Never mutate the state. Never mutate the state. Let Redux do its thing.

With that out of the way, the code here isn’t too bad.

First, we import our action types. Then we define a _nullSession, so that we can always call .username or .userId on our session object and not error out if there isn’t a user logged in.

Next, we give our incoming state a default value—our null session—and destructure the action object.

We also freeze the state, which prevents any accidental mutation and lets others know when they are working with our code to, “BACK OFF!”

Just kidding. But it makes it clear.

Lastly, depending on the action type, we return code that may or may not change our Redux store.

Our errors reducer will be very similar. reducers/errors/errors.js:

--- frontend/reducers/errors/errors.js ---

import { RECEIVE_CURRENT_USER } from "../../actions/session";

import { CLEAR_ERRORS, RECEIVE_ERRORS } from "../../actions/error"; export default (state = "", { message, type }) => {

Object.freeze(state);

switch (type) {

case RECEIVE_ERRORS:

return message;

case RECEIVE_CURRENT_USER:

case CLEAR_ERRORS:

return "";

default:

return state;

}

};

The only difference here is that when we receive our current user, we also clear errors. This is often how you’ll see different actions triggering different parts of the state.

Alas, we’re almost done satisfying Redux…

Let’s create our store/store.js:

--- frontend/store/store.js ---

import { createStore, applyMiddleware } from "redux";

import thunk from "redux-thunk";

import reducer from "../reducers/root"; export default preloadedState => (

createStore(

reducer,

preloadedState,

applyMiddleware(thunk)

)

);

This is what brings everything together. createStore, well, creates our Redux store, which houses the frontend state we’ve so carefully crafted. Redux Thunk is the middleware that helps us streamline our asynchronous actions. We export a function that takes in a preloaded-state as an argument and returns our store. This preloadedState is how we will keep our user logged in.

Let’s plug Redux into our App now by heading over to our index.js:

--- frontend/src/index.js ---

import React from 'react';

import ReactDOM from 'react-dom';

import App from './components/App';

import configureStore from './store/store';

import { Provider } from "react-redux"; let preloadedState = {};

const store = configureStore(preloadedState); ReactDOM.render(

<Provider store={store}>

<App />

</Provider>,

document.getElementById("root")

);

// FOR TESTING, remove before production

window.getState = store.getState;

We import the function we created in store.js and the Provider. We let our pre-loaded state equal an empty object for now and define our store. By wrapping our App in the Provider and passing the store as a prop, we gain access to our Redux store anywhere in our application. Props can be thought of as arguments provided to our components, as they are accessed via the prop object.

Lastly, we save the getState method onto the window so we can see our state in the console, however for larger apps, I recommend the redux-logger.

Let’s head over to http://localhost:3000/ to make sure everything still works and try out our getState function.

Using the Google Chrome developer tools,

Windows: CTRL-SHIFT-J or F12

Mac: ⌥-⌘-J ,

we can see our state!

Redux is now ready to rock!

And that concludes step 6! 😅

Step 7: Components

This is the last step. Our journey is almost complete.

All we have to do is create a few components, add protections to some of those components, and implement a way to keep our user signed in if they refresh their page.

Let’s start out with the components—there will be five of total including App.js.

Create a components/Welcome.js:

--- frontend/src/components/Welcome.js ---

import React from 'react';

import { Link } from 'react-router-dom'; export default () => (

<>

<h1>Welcome!</h1>

<Link to='/login'>Login</Link>

<Link to='/signup'>Signup</Link>

<Link to='/dashboard'>Dashboard</Link>

</>

);

As always, we start off with our imports. Link acts just like an anchor tag except its path is absolute. Other than that, we use another fragment, add a welcome message and three links. Easy-peasy.

Next in line is components/Signup.js. I’ll split it in three:

--- frontend/src/components/Signup.js ---

import React from "react";

import { connect } from "react-redux";

import { Link } from "react-router-dom";

import { signup } from "../actions/session"; const mapStateToProps = ({ errors }) => ({

errors

}); const mapDispatchToProps = dispatch => ({

signup: user => dispatch(signup(user))

});

...

The only newcomers in our imports is connect. This function will allow our component to have access to our Redux store. We then define two functions:

mapStateToProps; this takes state as a parameter and gives our components access to specific parts of our Redux store (in this case our errors).

mapDispatchToProps; this takes dispatch as a parameter and gives our component the ability to dispatch the actions we defined earlier.

Let’s continue:

--- frontend/src/components/Signup.js ---

...

const Signup = ({ errors, signup }) => {

const handleSubmit = e => {

e.preventDefault();

const user = {

username: e.target[0].value,

email: e.target[1].value,

password: e.target[2].value

};

signup(user);

}; return (

<>

<h1>Signup</h1>

<p>{errors}</p>

<form onSubmit={handleSubmit}>

<label>

Username:

<input type="text" name="username" />

</label>

<label>

Email:

<input type="email" name="email" />

</label>

<label>

Password:

<input type="password" name="password" />

</label>

<input type="submit" value="Submit" />

</form>

<Link to="/login">Login</Link>

</>

);

};

...

It looks a little scary, but not much is really going on here. This is our actual component in this file. First, we take in props as an argument and destructure what we need. These came from mapStateToProps/mapDispatchToProps.

Next, we create a submit handler function, where “e” is the event object. We prevent the default behavior (which refreshes the page), create a user object by extracting the input values from our form, and invoke our login function.

Normally, you’d want a controlled component here, but we’re keeping things simple.

Lastly, we return our JSX. In this case, it’s just a header, our errors*, a form, and another link. *These are the errors from our Redux store. If there isn’t any, React won’t render anything. The curly brackets are how we add JavaScript code inside a component return statement.

Notice the <form onSubmit={handleSubmit}> ? This puts an onsubmit event on our form, passing the function we defined above as the callback.

Last little bit:

--- frontend/src/components/Signup.js ---

...

export default connect(

mapStateToProps,

mapDispatchToProps

)(Signup);

This is how the connection happens. Here we are “connecting” our Signup component to the Redux store.

Now, let’s create our components/Login.js file:

--- frontend/src/components/Login.js ---

import React from "react";

import { connect } from "react-redux";

import { Link } from "react-router-dom";

import { login } from "../actions/session"; const mapStateToProps = ({ errors }) => ({

errors

}); const mapDispatchToProps = dispatch => ({

login: user => dispatch(login(user))

}); const Login = ({ errors, login }) => {

const handleSubmit = e => {

e.preventDefault();

const user = {

email: e.target[0].value,

password: e.target[1].value,

};

login(user);

} return (

<>

<h1>Login</h1>

<p>{errors}</p>

<form onSubmit={handleSubmit}>

<label>

Email:

<input type="email" name="email" />

</label>

<label>

Password:

<input type="password" name="password" />

</label>

<input type="submit" value="Submit" />

</form>

<Link to="/signup">Signup</Link>

</>

);

}; export default connect(

mapStateToProps,

mapDispatchToProps

)(Login);

The only difference here is we import login instead of signup and have one less form field.

Nice work!

Let’s move on to our components/Dashboard.js:

--- frontend/src/components/Dashboard.js ---

import React from "react";

import { connect } from "react-redux";

import { logout } from "../actions/session"; const mapStateToProps = ({ session }) => ({

session

}); const mapDispatchToProps = dispatch => ({

logout: () => dispatch(logout())

}); const Dashboard = ({ logout, session }) => (

<>

<h1>Hi {session.username}</h1>

<p>You are now logged in!</p>

<button onClick={logout}>Logout</button>

</>

); export default connect(

mapStateToProps,

mapDispatchToProps

)(Dashboard);

Notice a pattern? We do the same thing mapping state and dispatch to props and connecting our component at the bottom. We use the session info to greet the user by username and add a button that logs the user out.

Lastly, we update our App.js:

--- frontend/src/components/App.js ---

import React from "react";

import { Route } from "react-router-dom";

import Welcome from "./Welcome";

import Login from "./Login";

import Signup from "./Signup";

import Dashboard from "./Dashboard"; export default () => (

<>

<Route exact path="/" component={Welcome} />

<Route path="/login" component={Login} />

<Route path="/signup" component={Signup} />

<Route path="/dashboard" component={Dashboard} />

</>

);

🤔 Well, we import our components, but what is this Route thing? The Route component takes in a component prop that it renders when the URL matches the path. The “exact” flag specifies that the path must be exactly “/”. Without that, our Welcome component would render on every page, because all paths begin with “/”. (This can be useful for Nav components though!)

However, in order to use Route, we must wrap our App in it, just like we did with the Provider:

--- frontend/src/index.js ---

...

import { BrowserRouter } from "react-router-dom";

...

ReactDOM.render(

<Provider store={store}>

<BrowserRouter>

<App />

</BrowserRouter>

</Provider>,

document.getElementById("root")

);

...

Testing things out in the browser we can see our routes work and pages work.

Let’s test out our errors:

Nice! Okay, what if I try to signup? It appears nothing happens… Or does it?

If we look at our state again in the Chrome console, we can see that it does log the user in. Also if we take a look at our cookies:

I’m using the Edit This Cookie Chrome extension, but this can also be viewed using the Chrome developer tools.

We see that our cookie is there!

If I navigate to the dashboard (by manually typing the URL) and log out, the cookie is gone. Our backend work!

But our site is kind of broken…

Let’s fix this.

Our next step is to create protectedRoutes and authRoutes. The protected routes will not allow a logged out user to access them and will redirect them to the login page. The auth routes will not allow a user who is logged in to visit the login/signup pages. If they do, we redirect them to the dashboard.

Conceptually, this sounds simple, but the code here is a little on the complex side. Fear not, we’re in the final stretch—we can’t give up now!

Let’s dive right in. Create a route.js file in your util/ folder:

--- frontend/src/util/route.js ---

import React from "react";

import { connect } from "react-redux";

import { Redirect, Route, withRouter } from "react-router-dom"; const mapStateToProps = ({ session: { userId} }) => ({

loggedIn: Boolean(userId)

});

...

Nothing too crazy yet. Two newcomers in our imports are Redirect and withRouter. The first simply redirects to a URL of our choice. The second is used just like connect except it provides our component with useful props that pertain to routes/params/location/etc… We won’t be using any of these properties, but I add withRouter here because, ideally, we want that functionality in our app.

Next, we visit our good friend mapStateToProps. We deeply destructure our state object and provide a prop that simply answers the question, “are we logged in?” Let’s continue:

--- frontend/src/util/route.js ---

...

const Auth = ({ loggedIn, path, component: Component }) => (

<Route

path={path}

render={props => (

loggedIn ?

<Redirect to='/dashboard' /> :

<Component {...props} />

)}

/>

); const Protected = ({ loggedIn, path, component: Component }) => (

<Route

path={path}

render={props => (

loggedIn ?

<Component {...props} /> :

<Redirect to='/login' />

)}

/>

);

...

Syntactically, I’ve tried to make this as readable as possible. Both of these functions are nearly identical—the only difference is the Redirect.

Let’s break it down. At a high level, these are just components that implicitly return a Route component, that potentially renders another component or redirects.

We start off by destructuring our props. The component: Component is needed so React knows that this is an entire component coming in as a prop.

Next, we set up our Route by using our supplied path.

Then, instead of using component={SomeComponent} , we use the render prop, which allows us to supply an anonymous function that uses a ternary to either display our component or redirect.

We also make use of the spread operator to pass our props down to the rendered component. There’s one more step:

--- frontend/src/util/route.js ---

...

export const AuthRoute = withRouter(

connect(mapStateToProps)(Auth)

); export const ProtectedRoute = withRouter(

connect(mapStateToProps)(Protected)

);

We wrap everything in withRouter and connect these components.

Now back in our App.js:

--- frontend/src/util/routes.js ---

...

import { AuthRoute, ProtectedRoute } from "../util/route"; export default () => (

<>

<Route exact path="/" component={Welcome} />

<AuthRoute path="/login" component={Login} />

<AuthRoute path="/signup" component={Signup} />

<ProtectedRoute path="/dashboard" component={Dashboard} />

</>

);

Notice how easy they are to use? We can use them just like a normal Route! Plus, now we always have these at our disposal.

Now go to http://localhost:3000/ and test them out.

If everything went well, you should not be able to access the Dashboard.

Sweet! There’s only one more thing to do—and it’s easy!

In our util/session.js let’s add one more function:

--- frontend/src/util/session.js ---

...

export const checkLoggedIn = async preloadedState => {

const response = await fetch('/api/session');

const { user } = await response.json();

let preloadedState = {};

if (user) {

preloadedState = {

session: user

};

}

return preloadedState;

};

Here we hit our last API endpoint to check if a session exists.

Now in our src/index.js:

--- frontend/src/index.js ---

...

import { checkLoggedIn } from "./util/session"; const renderApp = preloadedState => {

const store = configureStore(preloadedState);

ReactDOM.render(

<Provider store={store}>

<BrowserRouter>

<App />

</BrowserRouter>

</Provider>,

document.getElementById("root")

);

}; (async () => renderApp(await checkLoggedIn()))();

First, we encapsulate everything that has to do with rendering our App into a function.

Then we use another IIFE, just like we did in our server.js. Using the return value of checkLoggedIn, our renderApp function has everything it needs to start up our application.

But enough talk, let’s test it out!

After logging in, we are automatically redirected to the dashboard (because the login/signup page are AuthRoutes).

We can refresh and stay logged in!

After logging out, we are automatically redirected to the login page (because the dashboard page is a ProtectedRoute).

No errors, red lights, or trouble in our console and everything works smoothly.

🏆 MISSION ACCOMPLISHED 🏆

I know this article is quite lengthy, and this is my first time writing anything online, but I really enjoyed this experience.

Of course, more than anything I hope YOU enjoyed this!

See you next time!

View the codebase on GitHub.

More about me.