MERN Crash Course (Part 2) - React Frontend UI, State Management With Redux Toolkit

May 15, 2023
MERN Course Tutorial

This is part two of a two-part series on how to implement a MERN stack app with an authentication system using JWTs and HTTP-only cookies. In this part, we will be creating the frontend React application to consume the API we built in part one. We will use Redux Toolkit for state management.

Create A New React App

We are going to use Vite to create our React SPA (Single Page Application). Vite is a new build tool that is much faster than Create React App. It is also very easy to use. To install Vite, run the following command from the root of your project:

npm create vite@latest frontend

This will create a new folder called frontend and install all the dependencies for a React app.

Go into the frontend folder and run npm install to install all the dependencies.

If you are using Git, you should move the .gitignore file from the frontend folder to the root of your project. Replace the one you already have there, just add .env to the new one.

Vite Config

Open the vite.config.js file and add a server object with the following values:

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:5000',
        changeOrigin: true,
      },
    },
  },
});

This will make the server start on port 3000 and proxy all requests to /api to port 5000. This is the port our backend server will be running on.

Client Script

I want to be able to run the frontend React dev server from the root directory. So in the ROOT package.json, add the following script:

"scripts": {
   "client": "npm run dev --prefix frontend",
}

Concurrently Setup

I also want a script that will run both the backend api along with the frontend dev server. To do this, we will use the concurrently package. From the root, install it with the following command:

npm install -D concurrently

Then we want to add a script to run both the front and backend. The scripts in your package.json should look like this:

"scripts": {
    "start": "node backend/server.js",
    "server": "nodemon backend/server.js",
    "client": "npm start --prefix frontend",
    "dev": "concurrently \"npm run server\" \"npm run client\""
  },

Now fron the root, you can run npm run dev and it will start both the backend and frontend dev servers.

You should see the landing page at http://localhost:3000/ and you should be able to access the api on port 5000.

Clean Up

I usually like to clean up some of the boilerplate code. Vite doesn't have much, but I will delete the App.css file and clear out the index.css file.

Open up App.jsx and just make it a simple component with an h1 tag that says "Hello World".

const App = () => {
  return <h1>Hello World</h1>;
};

export default App;

Install React Bootstrap

We are going to use Bootstrap and the React Bootstrap UI library, which allows us to use Bootrtap Components as React components. We will also use the react-icons package for font awesome icons. From the frontend folder, run the following command:

npm install react-bootstrap bootstrap react-icons

Then add the following import to the main.jsx file:

import 'bootstrap/dist/css/bootstrap.min.css';

Create Header

Let's create a basic header/navbar component. Create a new folder called components and inside that folder create a new file called Header.jsx.

import { Navbar, Nav, Container } from 'react-bootstrap';
import { FaSignInAlt, FaSignOutAlt } from 'react-icons/fa';

const Header = () => {
  return (
    <header>
      <Navbar bg='dark' variant='dark' expand='lg' collapseOnSelect>
        <Container>
          <Navbar.Brand href='/'>MERN App</Navbar.Brand>
          <Navbar.Toggle aria-controls='basic-navbar-nav' />
          <Navbar.Collapse id='basic-navbar-nav'>
            <Nav className='ms-auto'>
              <Nav.Link href='/login'>
                <FaSignInAlt /> Sign In
              </Nav.Link>
              <Nav.Link href='/login'>
                <FaSignOutAlt /> Sign Up
              </Nav.Link>
            </Nav>
          </Navbar.Collapse>
        </Container>
      </Navbar>
    </header>
  );
};

export default Header;

HomeScreen

Create a folder called screens and inside that folder create a new file called HomeScreen.jsx. For now, just add the following:

const HomeScreen = () => {
  return <div>HomeScreen</div>;
};
export default HomeScreen;

HomeScreen Hero

To make this look a little less horrible, let's add a hero section to the HomeScreen component. Create a new file in the components folder called Hero.jsx. Add the following code:

import { Container, Card, Button } from 'react-bootstrap';

const Hero = () => {
  return (
    <div className=' py-5'>
      <Container className='d-flex justify-content-center'>
        <Card className='p-5 d-flex flex-column align-items-center hero-card bg-light w-75'>
          <h1 className='text-center mb-4'>MERN Authentication</h1>
          <p className='text-center mb-4'>
            This is a boilerplate for MERN authentication that stores a JWT in
            an HTTP-Only cookie. It also uses Redux Toolkit and the React
            Bootstrap library
          </p>
          <div className='d-flex'>
            <Button variant='primary' href='/login' className='me-3'>
              Sign In
            </Button>
            <Button variant='secondary' href='/register'>
              Register
            </Button>
          </div>
        </Card>
      </Container>
    </div>
  );
};

export default Hero;

Then import it into the HomeScreen.jsx file and add it to the component:

import Hero from '../components/Hero';

const HomeScreen = () => {
  return (
    <>
      <Hero />
    </>
  );
};

Open the App.js file and add the following code:

import { Container } from 'react-bootstrap';
import Header from '../components/Header';
import HomeScreen from './screens/HomeScreen';

const App = () => {
  return (
    <>
      <Header />
      <Container className='my-2'>
        <HomeScreen />
      </Container>
    </>
  );
};

export default App;

You should now see the HomeScreen with the Hero section:

React Router

We need to be able to have multiple pages in our app. To do this, we will use React Router. From the frontend folder, run the following command:

npm install react-router-dom

In the main.jsx file if you're using Vite and index.js if you're using Create React App, add the following code:

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import {
  createBrowserRouter,
  createRoutesFromElements,
  Route,
  RouterProvider,
} from 'react-router-dom';
import './index.css';
import 'bootstrap/dist/css/bootstrap.min.css';
import HomeScreen from './screens/HomeScreen';

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path='/' element={<App />}>
      <Route index={true} path='/' element={<HomeScreen />} />
    </Route>
  )
);

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

Here we are creating our routes and wrapping our app in the RouterProvider component. We also created the route for the HomeScreen component.

Now, we just need to add the <Outlet /> component to the App.jsx file. This will render the child routes of the parent route. So our App.jsx file should look like this:

import { Container } from 'react-bootstrap';
import { Outlet } from 'react-router-dom';
import Header from './components/Header';

const App = () => {
  return (
    <>
      <Header />
      <Container className='my-2'>
        <Outlet />
      </Container>
    </>
  );
};

export default App;

Right now, in our header, we have a link to the login and register pages using an anchor tag. This will cause a full page refresh when we click on the link. We want to use the Link component from React Router to prevent this.

Since we are using React Bootstrap, we can use the LinkContainer component from react-router-bootstrap to wrap our Link component. This will allow us to use the Link component as a React Bootstrap component.

From the frontend folder, run the following command:

npm install react-router-bootstrap

Now, change the Header.jsx file to the following:

import { Navbar, Nav, Container } from 'react-bootstrap';
import { LinkContainer } from 'react-router-bootstrap';
import { FaSignInAlt, FaSignOutAlt } from 'react-icons/fa';

const Header = () => {
  return (
    <header>
      <Navbar bg='dark' variant='dark' expand='lg' collapseOnSelect>
        <Container>
          <LinkContainer to='/'>
            <Navbar.Brand>MERN App</Navbar.Brand>
          </LinkContainer>
          <Navbar.Toggle aria-controls='basic-navbar-nav' />
          <Navbar.Collapse id='basic-navbar-nav'>
            <Nav className='ms-auto'>
              <LinkContainer to='/login'>
                <Nav.Link>
                  <FaSignInAlt /> Sign In
                </Nav.Link>
              </LinkContainer>
              <LinkContainer to='/register'>
                <Nav.Link>
                  <FaSignOutAlt /> Sign Up
                </Nav.Link>
              </LinkContainer>
            </Nav>
          </Navbar.Collapse>
        </Container>
      </Navbar>
    </header>
  );
};

export default Header;

Notice now we are wrapping the Navbar.Brand and Nav.Link components in the LinkContainer component. Also, we are using the to prop instead of the href prop.

I had to restart my server for some reason. So if you run into an issue, try that first.

FormContainer Component

Before we create our login and register forms, I want to create a simple wrapper component for these forms, since they will be in a more narrow container.

Create a new file in the components folder called FormContainer.jsx. Add the following code:

import { Container, Row, Col } from 'react-bootstrap';

const FormContainer = ({ children }) => {
  return (
    <Container>
      <Row className='justify-content-md-center mt-5'>
        <Col xs={12} md={6} className='card p-5'>
          {children}
        </Col>
      </Row>
    </Container>
  );
};

export default FormContainer;

This will allow us to wrap our forms in a container that is centered on the page and is only 6 columns wide on larger screens.

Login Screen

Let's go ahead and create the login screen. Create a new file in the screens folder called LoginScreen.jsx. Add the following code:

import { useState } from 'react';
import { Link } from 'react-router-dom';
import { Form, Button, Row, Col } from 'react-bootstrap';
import FormContainer from '../components/FormContainer';

const LoginScreen = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const submitHandler = async (e) => {
    e.preventDefault();
    console.log('submit');
  };

  return (
    <FormContainer>
      <h1>Sign In</h1>

      <Form onSubmit={submitHandler}>
        <Form.Group className='my-2' controlId='email'>
          <Form.Label>Email Address</Form.Label>
          <Form.Control
            type='email'
            placeholder='Enter email'
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          ></Form.Control>
        </Form.Group>

        <Form.Group className='my-2' controlId='password'>
          <Form.Label>Password</Form.Label>
          <Form.Control
            type='password'
            placeholder='Enter password'
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          ></Form.Control>
        </Form.Group>

        <Button type='submit' variant='primary' className='mt-3'>
          Sign In
        </Button>
      </Form>

      <Row className='py-3'>
        <Col>
          New Customer? <Link to={`/register`}>Register</Link>
        </Col>
      </Row>
    </FormContainer>
  );
};

export default LoginScreen;

This is strictly the UI. We have no functionality at all. I want to get both forms displaying before we add any functionality.

Add To Router

Open the main.jsx file and add the following route:

import LoginScreen from './screens/LoginScreen.jsx';

<Route path='/' element={<App />}>
  <Route index={true} path='/' element={<HomeScreen />} />
  <Route path='/login' element={<LoginScreen />} /> {/* Add this line */}
</Route>;

You should now see a basic login form:

Register Screen

Let's do the same with the register screen. Create the UI and add the route.

Create a new file in the screens folder called RegisterScreen.jsx. Add the following code:

import { useState } from 'react';
import { Link } from 'react-router-dom';
import { Form, Button, Row, Col } from 'react-bootstrap';
import FormContainer from '../components/FormContainer';

const RegisterScreen = () => {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');

  const submitHandler = async (e) => {
    e.preventDefault();
    console.log('submit');
  };

  return (
    <FormContainer>
      <h1>Register</h1>
      <Form onSubmit={submitHandler}>
        <Form.Group className='my-2' controlId='name'>
          <Form.Label>Name</Form.Label>
          <Form.Control
            type='name'
            placeholder='Enter name'
            value={name}
            onChange={(e) => setName(e.target.value)}
          ></Form.Control>
        </Form.Group>

        <Form.Group className='my-2' controlId='email'>
          <Form.Label>Email Address</Form.Label>
          <Form.Control
            type='email'
            placeholder='Enter email'
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          ></Form.Control>
        </Form.Group>

        <Form.Group className='my-2' controlId='password'>
          <Form.Label>Password</Form.Label>
          <Form.Control
            type='password'
            placeholder='Enter password'
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          ></Form.Control>
        </Form.Group>
        <Form.Group className='my-2' controlId='confirmPassword'>
          <Form.Label>Confirm Password</Form.Label>
          <Form.Control
            type='password'
            placeholder='Confirm password'
            value={confirmPassword}
            onChange={(e) => setConfirmPassword(e.target.value)}
          ></Form.Control>
        </Form.Group>

        <Button type='submit' variant='primary' className='mt-3'>
          Register
        </Button>
      </Form>

      <Row className='py-3'>
        <Col>
          Already have an account? <Link to={`/login`}>Login</Link>
        </Col>
      </Row>
    </FormContainer>
  );
};

export default RegisterScreen;

Now add the route:

import RegisterScreen from './screens/RegisterScreen.jsx';

<Route path='/' element={<App />}>
  <Route index={true} path='/' element={<HomeScreen />} />
  <Route path='/login' element={<LoginScreen />}
  <Route path='/register' element={<RegisterScreen />} /> {/* Add this line */}
</Route>;

You should now see a basic register form:

Redux Toolkit

Now that we have our UI setup, we now need to deal with application state. We could send off a request from the login and register component, but I want a centralized place to handle all of our state and any requests that will change that state. This is where Redux comes in. Redux is a state management library that allows us to create a store that holds all of our state. We can then dispatch actions to change that state.

A reducer is a function that takes in the current state and an action and returns the new state. We can have multiple reducers that handle different parts of our state. For example, we could have a reducer that handles the user state and another reducer that handles the product state.

Redux Toolkit is a package that helps us create our store and reducers in a much easier way than using vanilla Redux.

Let's install Redux Toolkit as well as the React bindings for Redux. Make sure that you are in the frontend folder and run the following command:

cd frontend
npm install @reduxjs/toolkit react-redux

Store

A store is a place to hold all of our state. We can then dispatch actions to change that state. We can also subscribe to the store to get the current state.

Create a file in the frontend/src folder called store.js. Add the following code:

import { configureStore } from '@reduxjs/toolkit';

const store = configureStore({
  reducer: {},
  middleware: (getDefaultMiddleware) => getDefaultMiddleware(),
  devTools: true,
});

export default store;

We are using the configureStore function from Redux Toolkit to create our store. We are passing in an object with a reducer property. We will add our reducers to this object later. We are also passing in a middleware property. Middleware is a way to extend Redux with custom functionality. We are also enabling the Redux DevTools extension, which you should have installed, if you don't, get it from the following links:

Provider

In order for our application to work with the Redux store, we have to wrap it in our provider.

Open the main.jsx file and add the following code:

import store from './store';
import { Provider } from 'react-redux';

Then wrap the app with the provider:

ReactDOM.createRoot(document.getElementById('root')).render(
  <Provider store={store}>
    <React.StrictMode>
      <RouterProvider router={router} />
    </React.StrictMode>
  </Provider>
);

I hit another error here and I actually had to delete my frontend node_modules folder and run npm install again. I'm not sure why this happened, but it fixed the issue.

You will probably get a warning in the console that says you do not have a valid reducer, but that is fine at this point.

Auth Slice

In Redux Toolkit, we create reducers using something called a slice. A slice is a collection of reducer logic and actions for a single feature of our app. We will create a slice for our authentication that will only deal with the local storage of the user. We will have a separate API slice for actually making the requests to the endpoint.

Create a folder in the frontend/src folder called slices. Then create a file in that folder called authSlice.js. Add the following code:

import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  userInfo: localStorage.getItem('userInfo')
    ? JSON.parse(localStorage.getItem('userInfo'))
    : null,
};

const authSlice = createSlice({
  name: 'auth',
  initialState,
  reducers: {
    setCredentials: (state, action) => {
      state.userInfo = action.payload;
      localStorage.setItem('userInfo', JSON.stringify(action.payload));
    },
    logout: (state, action) => {
      state.userInfo = null;
      localStorage.removeItem('userInfo');
    },
  },
});

export const { setCredentials, logout } = authSlice.actions;

export default authSlice.reducer;

Like I said, this only has to do with the local storage of the user. We have one reducer that sets the user info in local storage and another that removes it. Very simple.

To use a slice, we need to import it into the store and use it. Open the store.js file and add the following code:

import { configureStore } from '@reduxjs/toolkit';
import authReducer from './slices/AuthSlice';

const store = configureStore({
  reducer: {
    auth: authReducer,
  },
  middleware: (getDefaultMiddleware) => getDefaultMiddleware(),
  devTools: true,
});

export default store;

So just bring in authSlice and add it to the reducer object.

Now, open your devtools and go to the Redux tab. You should see your store with the auth slice:

The userinfo is currently empty, but after we authenticate with our backend, it will store the logged in user data. It will also be in local storage.

API Slice

In order to make async requests from our slice, we need to use something called createAsyncThunk. This can be a bit confusing but Redux handles this behind the scenes as long as we follow the correct conventions.

Create a file in the frontend/src/slices folder called apiSlice.js. Add the following code:

import { fetchBaseQuery, createApi } from '@reduxjs/toolkit/query/react';

const baseQuery = fetchBaseQuery({ baseUrl: '' });

export const apiSlice = createApi({
  baseQuery,
  tagTypes: ['User'],
  endpoints: (builder) => ({}),
});

We are using the createApi function from Redux Toolkit to create our API slice instead of createSlice, because it includes the middleware that we need to make requests to our server. We are passing in a baseQuery object that will be used to make our requests. We are also passing in an endpoints object that will hold all of our endpoints.

We need to hook this up to our store, so open the store.js file and add the following code:

import { configureStore } from '@reduxjs/toolkit';
import { apiSlice } from './slices/apiSlice';
import cartSliceReducer from './slices/cartSlice';
import authReducer from './slices/authSlice'; // add this line

const store = configureStore({
  reducer: {
    [apiSlice.reducerPath]: apiSlice.reducer,
    cart: cartSliceReducer,
    auth: authReducer, // add this line
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(apiSlice.middleware),
  devTools: true,
});

export default store;

We brought in the new API slice and added it to the reducer object. We also added the middleware to the getDefaultMiddleware function by using the concat method.

These few lines of code do so much for us behind the scenes. They allow us to easily create queries and mutations along with the loading and error state. Something we would have to manually take care of if we used vanilla Redux.

User API Slice

We want to work with our user data and we need to create some endpoints to work with the backend. We are going to keep that stuff in a separate file.

Create a file called usersApiSlice.js in the frontend/src/slices folder. Add the following code:

import { apiSlice } from './apiSlice';
const USERS_URL = '/api/users';

export const userApiSlice = apiSlice.injectEndpoints({
  endpoints: (builder) => ({
    login: builder.mutation({
      query: (data) => ({
        url: `${USERS_URL}/auth`,
        method: 'POST',
        body: data,
      }),
    }),
  }),
});

export const { useLoginMutation } = userApiSlice;

We bring in the apiSlice and then use the injectEndpoints method to create our endpoints. We are only creating one endpoint for now, but we will add more later. We are using the mutation method to create a login endpoint. We are passing in a query object that contains the url, method, and body of the request.

As you can see, when we need to interact with our backend, it's as simple as creating an endpoint and passing in the data. We don't have to worry about the loading and error state, because Redux Toolkit handles that for us.

We also do not need to add this slice to the store because it is already included in the apiSlice that we added to the store. You can think of this as a sub-slice to the API slice.

Using the Login Endpoint

Now we want to hook this mutation up to our login form, so open up the frontend/src/components/LoginForm.js file and add the following code:

import { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Form, Button, Row, Col } from 'react-bootstrap';
import FormContainer from '../components/FormContainer';
import { useDispatch, useSelector } from 'react-redux';
import { useLoginMutation } from '../slices/usersApiSlice';
import { setCredentials } from '../slices/authSlice';

const LoginScreen = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const dispatch = useDispatch();
  const navigate = useNavigate();

  const [login, { isLoading }] = useLoginMutation();

  const { userInfo } = useSelector((state) => state.auth);

  useEffect(() => {
    if (userInfo) {
      navigate('/');
    }
  }, [navigate, userInfo]);

  const submitHandler = async (e) => {
    e.preventDefault();
    try {
      const res = await login({ email, password }).unwrap();
      dispatch(setCredentials({ ...res }));
      navigate('/');
    } catch (err) {
      console.log(err?.data?.message || err.error);
    }
  };

  return (
    <FormContainer>
      <h1>Sign In</h1>

      <Form onSubmit={submitHandler}>
        <Form.Group className='my-2' controlId='email'>
          <Form.Label>Email Address</Form.Label>
          <Form.Control
            type='email'
            placeholder='Enter email'
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          ></Form.Control>
        </Form.Group>

        <Form.Group className='my-2' controlId='password'>
          <Form.Label>Password</Form.Label>
          <Form.Control
            type='password'
            placeholder='Enter password'
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          ></Form.Control>
        </Form.Group>

        <Button
          disabled={isLoading}
          type='submit'
          variant='primary'
          className='mt-3'
        >
          Sign In
        </Button>
      </Form>

      {isLoading && <p>Loading...</p>}

      <Row className='py-3'>
        <Col>
          New Customer? <Link to='/register'>Register</Link>
        </Col>
      </Row>
    </FormContainer>
  );
};

export default LoginScreen;

We are doing quite a bit here. First, we are bringing in useSelector and useDispatch from react-redux. This allows us to dispatch actions and select data from the store. We are also bringing in the useLoginMutation hook that we created in the usersApiSlice.js file. We are also bringing in the setCredentials action from the authSlice.js file. We will set the credentials after a successful login.

Then we are getting the login function and the isLoading state from the useLoginMutation hook. We are also getting the userInfo state from the authSlice.js file.

We are using the useEffect hook to redirect the user to the home page if they are already logged in. We are also using the useNavigate hook from react-router-dom to redirect the user after a successful login.

Try and submit with either empty fields or the wrong login creds. You should see a console log with an error. I want to use the React Toastify package to show errors. I also want to show a loading spinner when the login button is clicked. Let's handle that before we test with the correct login creds.

React Toastify

We are going to use the React Toastify package to show errors and success messages. This is a great package that is easy to use and looks great. Let's install it now. Make sure that you are in the frontend folder and run the following command:

npm i react-toastify

Now open up the frontend/src/App.js file and replace with the following code:

import { Container } from 'react-bootstrap';
import { Outlet } from 'react-router-dom';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import Header from './components/Header';

const App = () => {
  return (
    <>
      <Header />
      <ToastContainer />
      <Container className='my-2'>
        <Outlet />
      </Container>
    </>
  );
};

export default App;

Now in the frontend/src/components/LoginForm.js file, add the following code:

import { toast } from 'react-toastify';

Replace the console.log in the catch block with the following code:

toast.error(err?.data?.message || err.error);

Now, when you try and submit with the wrong login creds, you should see a nice error message. You can also use the toast.success method to show a success message.

Loading Spinner

Let's create a file in the frontend/src/components folder called Loader.jsx and add the following code:

import { Spinner } from 'react-bootstrap';

const Loader = () => {
  return (
    <Spinner
      animation='border'
      role='status'
      style={{
        width: '100px',
        height: '100px',
        margin: 'auto',
        display: 'block',
      }}
    ></Spinner>
  );
};

export default Loader;

We are using the Spinner component from react-bootstrap to create a loading spinner. We are also using some inline styles to center the spinner.

Now bring it into the LoginScreen component and replace the paragraph tag:

import Loader from '../components/Loader';

{
  isLoading && <Loader />;
}

Now when you click the login button, you should see a loading spinner for a few milliseconds.

Setting Credentials

Now, let's try logging in with a user with the correct email and password.

You should be redirected to the home screen and if you open your Redux dev tools, you should see the auth state with the userInfo object.

This happens because once we get a successful login, we call the setCredentials action and pass in the userInfo object. This will set the userInfo object in the auth state. It is also saved in local storage.

What is really cool is now the cookie is set with the JWT. If you open up the dev tools and go to the Application tab, you should see the cookie.

This is an HTTP-Only cookie and is a safer place for the JWT and will be sent with every request.

If you try and go to the sign in page, you will be redirected to the homepage.

I want to make it so that if we are logged in, we see a dropdown with the user's name and a link to their profile and a logout link.

Open up the frontend/src/components/Header.js file and replace with the following code:

import { Navbar, Nav, Container, NavDropdown, Badge } from 'react-bootstrap';
import { FaSignInAlt, FaSignOutAlt } from 'react-icons/fa';
import { LinkContainer } from 'react-router-bootstrap';
import { useSelector, useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';

const Header = () => {
  const { userInfo } = useSelector((state) => state.auth);

  return (
    <header>
      <Navbar bg='dark' variant='dark' expand='lg' collapseOnSelect>
        <Container>
          <LinkContainer to='/'>
            <Navbar.Brand>MERN App</Navbar.Brand>
          </LinkContainer>
          <Navbar.Toggle aria-controls='basic-navbar-nav' />
          <Navbar.Collapse id='basic-navbar-nav'>
            <Nav className='ms-auto'>
              {userInfo ? (
                <>
                  <NavDropdown title={userInfo.name} id='username'>
                    <LinkContainer to='/profile'>
                      <NavDropdown.Item>Profile</NavDropdown.Item>
                    </LinkContainer>
                    <NavDropdown.Item>Logout</NavDropdown.Item>
                  </NavDropdown>
                </>
              ) : (
                <>
                  <LinkContainer to='/login'>
                    <Nav.Link>
                      <FaSignInAlt /> Sign In
                    </Nav.Link>
                  </LinkContainer>
                  <LinkContainer to='/register'>
                    <Nav.Link>
                      <FaSignOutAlt /> Sign Up
                    </Nav.Link>
                  </LinkContainer>
                </>
              )}
            </Nav>
          </Navbar.Collapse>
        </Container>
      </Navbar>
    </header>
  );
};

export default Header;

We are bringing in the userInfo from the auth state. This is essentially the data that we have in local storage. We are also bringing in the useDispatch hook from react-redux and the useNavigate hook from react-router-dom. We will need these soon.

Logout

Let's make the logout function next. We already have the backend functionality.

Open the usersApiSlice.js file and add the following function under the login function:

  logout: builder.mutation({
      query: () => ({
        url: `${USERS_URL}/logout`,
        method: 'POST',
      }),
    }),

Don't forget to export:

export const { useLoginMutation, useLogoutMutation } = userApiSlice;

This is very simple, we are just making a POST request to the /logout endpoint. Remember, it kills the cookie on the server. We also need to kill the local storage and the Redux state. That function was already added to the authSlice.js file.

Now open up the frontend/src/components/Header.js file and add the following code:

import { useLogoutMutation } from '../slices/usersApiSlice';
import { logout } from '../slices/authSlice';

Add the following code right under where we get the userInfo:

const dispatch = useDispatch();
const navigate = useNavigate();

const [logoutApiCall] = useLogoutMutation();

const logoutHandler = async () => {
  try {
    await logoutApiCall().unwrap();
    dispatch(logout());
    navigate('/login');
  } catch (err) {
    console.error(err);
  }
};

Here we are bringing in the logout mutation from the usersApiSlice and the logout action from the authSlice. We are also inititalizing the useNavigate and useDispatch hooks.

We get the logoutApiCall function, which we could have called anything and then added the logout handler to call both the logout mutation and the logout action. We also navigate to the login page.

Now, just add the onClick handler to the logout link:

<NavDropdown.Item onClick={logoutHandler}>Logout</NavDropdown.Item>

Now, when you click the logout link, you should be redirected to the login page and the cookie should be removed. The user should also be removed from local storage and the Redux state.

Register

Before we get to the profile page, let's make the register page function.

Let's start by adding the mutation to the state. Open the usersApiSlice.js file and add the following code under the logout function:

  register: builder.mutation({
    query: (data) => ({
      url: `${USERS_URL}`,
      method: 'POST',
      body: data,
    }),
   }),

Export it:

export const { useLoginMutation, useLogoutMutation, useRegisterMutation } =
  userApiSlice;

Now open up the frontend/src/screens/RegisterScreen.js file and add the following code:

import { useState, useEffect } from 'react';
import { Form, Button, Row, Col } from 'react-bootstrap';
import FormContainer from '../components/FormContainer';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { useRegisterMutation } from '../slices/usersApiSlice';
import { setCredentials } from '../slices/authSlice';
import { toast } from 'react-toastify';

Add this right under all of the useState hooks:

const dispatch = useDispatch();
const navigate = useNavigate();

const [register, { isLoading }] = useRegisterMutation();

const { userInfo } = useSelector((state) => state.auth);

useEffect(() => {
  if (userInfo) {
    navigate('/');
  }
}, [navigate, userInfo]);

Add the following code to the submitHandler function:

const submitHandler = async (e) => {
  e.preventDefault();

  if (password !== confirmPassword) {
    toast.error('Passwords do not match');
  } else {
    try {
      const res = await register({ name, email, password }).unwrap();
      dispatch(setCredentials({ ...res }));
      navigate('/');
    } catch (err) {
      toast.error(err?.data?.message || err.error);
    }
  }
};

We are doing the same thing as with the login except we are using the register mutation instead of the login mutation. We are also using the setCredentials action instead of the login action.

Add the loading check under the button:

{
  isLoading && <Loader />;
}

Go ahead and try to register a new user. It should log that user in as well. You can also logout.

Profile

Now we have full authentication working. Let's make the profile page.

Create a new screen called ProfileScreen.js in the frontend/src/screens folder. Add the following code:

import { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Form, Button } from 'react-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import FormContainer from '../components/FormContainer';
import { toast } from 'react-toastify';
import Loader from '../components/Loader';

const ProfileScreen = () => {
  const [email, setEmail] = useState('');
  const [name, setName] = useState('');
  const [password, setPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');

  const submitHandler = async (e) => {
    e.preventDefault();
    console.log('submit');
  };

  return (
    <FormContainer>
      <h1>Update Profile</h1>

      <Form onSubmit={submitHandler}>
        <Form.Group className='my-2' controlId='name'>
          <Form.Label>Name</Form.Label>
          <Form.Control
            type='name'
            placeholder='Enter name'
            value={name}
            onChange={(e) => setName(e.target.value)}
          ></Form.Control>
        </Form.Group>
        <Form.Group className='my-2' controlId='email'>
          <Form.Label>Email Address</Form.Label>
          <Form.Control
            type='email'
            placeholder='Enter email'
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          ></Form.Control>
        </Form.Group>
        <Form.Group className='my-2' controlId='password'>
          <Form.Label>Password</Form.Label>
          <Form.Control
            type='password'
            placeholder='Enter password'
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          ></Form.Control>
        </Form.Group>

        <Form.Group className='my-2' controlId='confirmPassword'>
          <Form.Label>Confirm Password</Form.Label>
          <Form.Control
            type='password'
            placeholder='Confirm password'
            value={confirmPassword}
            onChange={(e) => setConfirmPassword(e.target.value)}
          ></Form.Control>
        </Form.Group>

        <Button type='submit' variant='primary' className='mt-3'>
          Update
        </Button>
      </Form>
    </FormContainer>
  );
};

export default ProfileScreen;

This is just a simple form with inputs for the user data. We will add the user data soon, but let's add the route first.

Open the main.jsx file and add the following route:

import ProfileScreen from './screens/ProfileScreen.jsx';

<Route path='/' element={<App />}>
  <Route index={true} path='/' element={<HomeScreen />} />
  <Route path='/login' element={<LoginScreen />} />
  <Route path='/register' element={<RegisterScreen />} />
  <Route path='/profile' element={<ProfileScreen />} /> {/* Add this line */}
</Route>;

Now if you login, you should be able to go to the profile menu item and see the form.

Private Route

The issue that we have right now is that even if you logout and manually go to the profile page, you can still see the form. We need to make this a private route so that only logged in users can see it.

We can do this by creating a private route component. Create a file called PrivateRoute.jsx in the frontend/src/components folder. Add the following code:

import { Navigate, Outlet } from 'react-router-dom';
import { useSelector } from 'react-redux';

const PrivateRoute = () => {
  const { userInfo } = useSelector((state) => state.auth);
  return userInfo ? <Outlet /> : <Navigate to='/login' replace />;
};
export default PrivateRoute;

All we are doing here is checking if the user is logged in. If they are, we render the Outlet component, which will render the child components. If they are not logged in, we redirect them to the login page.

Now we have to use this in the main.jsx file. Add the following import:

import PrivateRoute from './components/PrivateRoute.jsx';

Then replace the current <Route> in the 'createRoutesFromElements' function with the following code:

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path='/' element={<App />}>
      <Route index={true} path='/' element={<HomeScreen />} />
      <Route path='/login' element={<LoginScreen />} />
      <Route path='/register' element={<RegisterScreen />} />
      <Route path='' element={<PrivateRoute />}>
        <Route path='/profile' element={<ProfileScreen />} />
      </Route>
    </Route>
  )
);

We are wrapping the PrivateRoute component around the ProfileScreen component. Now if you logout and try to go to the profile page, you will be redirected to the login page.

Fill Form

We have the user data in local storage, so we can fill the form with that. Open the frontend/src/screens/ProfileScreen.js file and add the following code:

const { userInfo } = useSelector((state) => state.auth);

useEffect(() => {
  setName(userInfo.name);
  setEmail(userInfo.email);
}, [userInfo.email, userInfo.name]);

We are getting the user from storage and then using the useEffect hook to set the name and email state when the component mounts.

Update User

The last major thing that we need to do is be able to update our user data.

We already have the endpoint, so let's add the mutation to the usersApiSlice.js file:

  updateUser: builder.mutation({
    query: (data) => ({
      url: `${USERS_URL}/profile`,
      method: 'PUT',
      body: data,
    }),
  }),

Export it:

export const {
  useLoginMutation,
  useLogoutMutation,
  useRegisterMutation,
  useUpdateUserMutation,
} = userApiSlice;

Now open the frontend/src/screens/ProfileScreen.js file and add the following code:

import { useUpdateUserMutation } from '../slices/usersApiSlice';
import { setCredentials } from '../slices/authSlice';

We ned to bring in the mutation that we just created along with the setCredentials action because if we change the email or name, the credentials in state have to be updated.

Initialize dispatch and the function to update along with the loading state:

const dispatch = useDispatch();

const [updateProfile, { isLoading }] = useUpdateUserMutation();

Finally, add the following code to the submitHandler function:

const submitHandler = async (e) => {
  e.preventDefault();
  if (password !== confirmPassword) {
    toast.error('Passwords do not match');
  } else {
    try {
      const res = await updateProfile({
        _id: userInfo._id,
        name,
        email,
        password,
      }).unwrap();
      dispatch(setCredentials({ ...res }));
      toast.success('Profile updated successfully');
    } catch (err) {
      toast.error(err?.data?.message || err.error);
    }
  }
};

We check the confirm password, then hit the backend endpoint to update the data, then set the credentials in state and show a success message.

Go ahead and try and update the name.

If you update the email or password, be sure to use the new email and password to login.

Prepare For Production

We are almost done. We just need to prepare our app for production. We need to build the frontend and then serve the static files from the backend.

Open the frontend folder and run the following command:

npm run build

This will create a folder in the frontend folder. If you used Vite, the folder will be called dist and if you used CRA, it will be called build. This is what we will serve from the backend.

Open the backend/server.js file and add the following code:

import path from 'path';

Replace this line of code:

app.get('/', (req, res) => {
  res.send('API is running...');
});

With this:

if (process.env.NODE_ENV === 'production') {
  const __dirname = path.resolve();
  app.use(express.static(path.join(__dirname, '/frontend/dist')));

  app.get('*', (req, res) =>
    res.sendFile(path.resolve(__dirname, 'frontend', 'dist', 'index.html'))
  );
} else {
  app.get('/', (req, res) => {
    res.send('API is running....');
  });
}

What this will do is serve the static files from the frontend/dist folder and then serve the index.html file for any other route. The API routes will still work. Be sure if you used CRA to replace frontend/dist with frontend/build.

You can test this by setting your NODE_ENV variable in the .env file to production and then running the server with npm start and open the browser to localhost:5000. You should see the React app. This is your production build.

That's it! We now have a fully functioning authentication system with MERN and Redux Toolkit. You can use this as a starting point for your own projects.

Deploy To Akamai/Linode

Like I said in the beginning of part 1, Linode has always been my go-to for projects where I want complete control of my server.

Here is a link to get you $100 credit: https://linode.com/traversy

I have a GitHub gist that you can find here. That will explain how to deploy to linode and all of the setup steps including creating your SSH keys (optional), getting the files on the server, setting up PM2, NGINX and more.

I hope you enjoyed this MERN tutorial. I know it was very long, so if you finished it, congrats! 

Stay connected with news and updates!

Join our mailing list to receive the latest news and updates from our team.
Don't worry, your information will not be shared.

We hate SPAM. We will never sell your information, for any reason.