Authentication is the act of verifying the identity of someone or something to determine whether the information about them is correct. Authentication deals with knowing the identity of a user. For example, Benedicta logs in with her email and password, and the server uses the password to authenticate Benedicta.

Authentication can be implemented via email and password, One Time Pin (OTP), face recognition, OAuth, fingerprint, magic links, and many more. As a React.js developer, learning how to implement most of the authentication methods including JWT authentication will put your ahead of the competition when applying for jobs.

This tutorial will give you a comprehensive overview of JSON Web Token authentication in React. You will learn how to validate HTML forms, make Axios HTTP requests, register new users, verify their emails, log users into their accounts, and log out the authentication users.

Related articles in this series:

  1. React.js and Axios: User Registration and Email Verification
  2. Forgot/Reset Password in React.js and Axios

More practice:

React.js and Axios User Registration and Email Verification

Prerequisites

Before continuing with this article, you should:

  • Have basic knowledge of JavaScript and TypeScript.
  • Some basic understanding of React.js will be beneficial.
  • Have Node.js installed on your machine

React.js and Axios User Authentication Overview

The example app contains the following routes:

  • /login – a public route for logging into the React.js app. On submission, Axios will make a POST request to the API to authenticate the user’s credentials. On success, the API will return access and refresh tokens that will be included in subsequent requests.
  • /register – a public route for registering new users with the app.
  • /verifyemail/:verificationCode – a public route for verifying the user’s email address.
  • /profile – a secure route for displaying the currently authenticated user’s credentials fetched from the API using the access token.
  • / – a public route for displaying a simple welcome message to users.

Register for an account

On the account registration page, the user will provide the necessary credentials and click on the signup button to create an account. The React.js app will then make an Axios HTTP POST request to the /api/auth/register endpoint on the server.

reactjs axios http post request register new user

The API server will then validate the credentials, add the user to the database, generate the HTML email verification template, and send it to the user’s email inbox.

email received from the backend server

Verify the email address

When the user clicks on the Verify Your Account button in the email, the user will be redirected to the React.js email verification page where the verification code will be automatically filled in the input field.

reactjs axios http get request verify email address

When the user clicks on the verify email button, the React app will make an Axios GET request to the /api/auth/verifyemail/:verificationCode endpoint on the server. The server will then validate the email verification code and update the user’s information in the database.

Sign in to the registered account

On the account login page, the user will provide the credentials used in registering for the account and click on the login button.

Next, React will make an Axios POST request to the /api/auth/login endpoint on the server to obtain access and refresh tokens.

reactjs axios http post request login user

Get authenticated user’s credentials

After the user has been authenticated by the server, React will make an Axios GET request to the /api/users/me endpoint with the access token to retrieve the user’s credentials.

get authenticated user's credentials

Step 1 – Run the Backend Server

Follow the steps below to start either the Golang or Node.js server.

Backend Server Built with Node.js

For full details about how to create the Node.js and PostgreSQL API server, see the post API with Node.js + PostgreSQL + TypeORM: Send Emails. But to get up and running in order to continue with this tutorial, follow the steps below:

  1. Download or clone the project source code from https://github.com/wpcodevo/node_typeorm.
  2. Open your terminal and change the Git branch to jwt_auth_verify_email
  3. Install all the required Yarn packages by running yarn install or yarn from the command line in the project root folder.
  4. Run docker-compose up -d from the command line in the project root folder to start the PostgreSQL and Redis Docker containers.
  5. Duplicate the example.env file and rename the copied one to .env .
  6. Uncomment the Nodemailer code in the src/app.ts file and start the Express server with yarn start to generate the Nodemailer SMTP test credentials. After that comment out the Nodemailer code again.
  7. Copy the SMTP credentials generated in the terminal and add them to the .env file. Save the src/app.ts file for the environment variables to be loaded into the Node.js environment.

Backend Server Built with Golang

For full details about how to create the Golang and MongoDB API server, see the post API with Golang + MongoDB: Send HTML Emails with Gomail. Follow the steps below to get the Golang server up and running.

  1. Download or clone the project source code from https://github.com/wpcodevo/golang-mongodb-api.
  2. Open the project with your IDE and change the Git branch to golang-mognodb-send-emails .
  3. Install all the required packages with go mod tidy .
  4. Run docker-compose up -d to start the MongoDB and Redis Docker containers.
  5. Duplicate the example.env file and rename the copied one to app.env .
  6. Add your SMTP credentials to the app.env file or create a mailtrap.io account and add the SMTP credentials to the environment variables file.
  7. Run go run main.go from the command line in the project root directory to start the Golang server on port 8000.

Step 2 – Setup the React.js Project with Tailwind CSS

We are going to use the Vite scaffolding tool to generate the React project. Vite is a front-end build tool similar to Webpack.

Run the following command to create the React.js boilerplate project. You can name the project reactjs-axios .


yarn create vite reactjs-axios --template react-ts
# or
npm create vite@latest reactjs-axios -- --template react-ts

After the project has been generated, run the following commands to get into the project directory and install all the dependencies.


cd reactjs-axios && yarn
# or
cd reactjs-axios && npm install

Optional: Open the package.json file and replace the dev script with "dev": "vite --host localhost --port 3000" . By default, Vite starts the development server on port 5173 but I always want my frontend application to run on port 3000.

Next, install Tailwind CSS and its peer dependencies with the following commands:


yarn add -D tailwindcss postcss autoprefixer
# or
npm install -D tailwindcss postcss autoprefixer

Next, run this command to generate the tailwind.config.cjs and postcss.config.cjs files:


yarn tailwindcss init -p
# or 
npx tailwindcss init -p

Next, replace the content of the tailwind.config.cjs file with the following:

tailwind.config.cjs


/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{vue,js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      colors: {
        'ct-dark-600': '#222',
        'ct-dark-200': '#e5e7eb',
        'ct-dark-100': '#f5f6f7',
        'ct-blue-600': '#2363eb',
        'ct-yellow-600': '#f9d13e',
      },
      fontFamily: {
        Poppins: ['Poppins, sans-serif'],
      },
      container: {
        center: true,
        padding: '1rem',
        screens: {
          lg: '1125px',
          xl: '1125px',
          '2xl': '1125px',
        },
      },
    },
  },
  plugins: [],
};


Also, replace the content of the src/index.css file with the following tailwind CSS directives:


@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');

@tailwind base;
@tailwind components;
@tailwind utilities;

html{
    font-family: 'Poppins', sans-serif;
}

Finally, you can start the Vite development server by running yarn dev or npm run dev .

Step 3 – Setup Axios and Zustand Store

In this section, you will set up Axios to make the HTTP request to the backend API server. In addition, you will set up Zustand to manage the React.js state in the application.

Create an Axios Instance

Axios is a promise-based HTTP client library that can be used to make requests in the browser and node.js. Apart from making HTTP requests with Axios, it can also be used to make GraphQL requests.

Install the Axios library with the following command:


yarn add axios
# or
npm install axios

With the Axios library installed, we can start using it in the project. Create a src/api/authApi.ts file and add the following code.

src/api/authApi.ts


import axios from "axios";

const BASE_URL = "http://localhost:8000/api/";

export const authApi = axios.create({
  baseURL: BASE_URL,
  withCredentials: true,
});

authApi.defaults.headers.common["Content-Type"] = "application/json";

We created an Axios instance with a config to prevent us from repeating those options in every request. For example, instead of providing the full URI for the HTTP calls, we can simply provide the route path without the base URL.

Create the API Response Types

Now create src/api/types.ts file and add the following TypeScript interfaces to help us type the API responses.

src/api/types.ts


export interface IUser {
  name: string;
  email: string;
  role: string;
  photo: string;
  _id: string;
  id: string;
  createdAt: string;
  updatedAt: string;
  __v: number;
}

export interface GenericResponse {
  status: string;
  message: string;
}

export interface ILoginResponse {
  status: string;
  access_token: string;
}

export interface IUserResponse {
  status: string;
  data: {
    user: IUser;
  };
}


Create a React.js Store with Zustand

Zustand is a new state management library that uses flux principles in JS to simplify global state management in React. It’s similar to Redux but it can be used in React or any other JavaScript framework like Vue, Angular, Svelte, or even Vanilla JS.

As you might know, Redux is the most popular state management library in React but it requires a lot of code for it to work. Therefore, generally speaking, Redux is great for relatively large projects that have their states being updated frequently. On the other hand, Zustand provides simple hooks to manage states without boilerplate code.

Run this command to install the Zustand library:


yarn add zustand
# or
npm install zustand

Now we need to define the store that will contain all the application states and functions to update the states. Create a src/store/index.ts file and add the following code:

src/store/index.ts


import create from 'zustand';
import { IUser } from '../api/types';

type Store = {
  authUser: IUser | null;
  requestLoading: boolean;
  setAuthUser: (user: IUser | null) => void;
  setRequestLoading: (isLoading: boolean) => void;
};

const useStore = create<Store>((set) => ({
  authUser: null,
  requestLoading: false,
  setAuthUser: (user) => set((state) => ({ ...state, authUser: user })),
  setRequestLoading: (isLoading) =>
    set((state) => ({ ...state, requestLoading: isLoading })),
}));

export default useStore;

The userStore contains the Zustand states and functions to update the states. The authUser property will hold the authenticated user’s credentials, it is initialized to null by default.

The requestLoading property will allow us to display a loading spinner in the UI when a request is made to the API server.

Step 4 – Create Reusable Components

In this section, you will create some reusable components with React and tailwind CSS but before that install these dependencies.


yarn add zod react-hook-form @hookform/resolvers tailwind-merge react-toastify react-router-dom

  • tailwind-merge – This library has a utility function to efficiently merge Tailwind CSS classes.
  • react-hook-form – A library for validating React.js forms.
  • zod – A typescript-first schema validation library.
  • react-toastify – A library that allows you to add notifications to your React.js app.

Input Field Component

To begin, let’s use React Hook Form to create a reusable form input with Tailwind CSS and TypeScript.

src/components/FormInput.tsx


import React from 'react';
import { useFormContext } from 'react-hook-form';

type FormInputProps = {
  label: string;
  name: string;
  type?: string;
};

const FormInput: React.FC<FormInputProps> = ({
  label,
  name,
  type = 'text',
}) => {
  const {
    register,
    formState: { errors },
  } = useFormContext();
  return (
    <div className=''>
      <label htmlFor={name} className='block text-ct-blue-600 mb-3'>
        {label}
      </label>
      <input
        type={type}
        placeholder=' '
        className='block w-full rounded-2xl appearance-none focus:outline-none py-2 px-4'
        {...register(name)}
      />
      {errors[name] && (
        <span className='text-red-500 text-xs pt-1 block'>
          {errors[name]?.message as string}
        </span>
      )}
    </div>
  );
};

export default FormInput;

Loading Spinner

Here, you will create a reusable spinner component with the tailwind-merge library, React, TypeScript, and tailwind CSS.

src/components/Spinner.tsx


import React from 'react';
import { twMerge } from 'tailwind-merge';
type SpinnerProps = {
  width?: number;
  height?: number;
  color?: string;
  bgColor?: string;
};
const Spinner: React.FC<SpinnerProps> = ({
  width = 5,
  height = 5,
  color,
  bgColor,
}) => {
  return (
    <svg
      role='status'
      className={twMerge(
        'w-5 h-5 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600',
        `w-${width} h-${height} ${color} ${bgColor}`
      )}
      viewBox='0 0 100 101'
      fill='none'
      xmlns='http://www.w3.org/2000/svg'
    >
      <path
        d='M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z'
        fill='currentColor'
      />
      <path
        d='M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z'
        fill='currentFill'
      />
    </svg>
  );
};

export default Spinner;

Button with a loader

Here, you will create a loading button with the spinner we created above.

src/components/LoadingButton.tsx


import React from "react";
import { twMerge } from "tailwind-merge";
import Spinner from "./Spinner";

type LoadingButtonProps = {
  loading: boolean;
  btnColor?: string;
  textColor?: string;
  children: React.ReactNode;
};

export const LoadingButton: React.FC<LoadingButtonProps> = ({
  textColor = "text-white",
  btnColor = "bg-ct-yellow-600",
  children,
  loading = false,
}) => {
  return (
    <button
      type="submit"
      className={twMerge(
        `w-full py-3 font-semibold rounded-lg outline-none border-none flex justify-center`,
        `${btnColor} ${loading && "bg-[#ccc]"}`
      )}
    >
      {loading ? (
        <div className="flex items-center gap-3">
          <Spinner />
          <span className="text-slate-500 inline-block">Loading...</span>
        </div>
      ) : (
        <span className={`${textColor}`}>{children}</span>
      )}
    </button>
  );
};

Step 5 – Axios POST Request Register User

The account registration page contains a simple registration form built with the React Hook Form library that contains fields for name, email, password, and password confirmation.

Form validation rules are defined with the Zod schema validation library which the React Hook Form library supports out of the box via the @hookform/resolvers package, for more info on Zod, see https://github.com/colinhacks/zod.

The onSubmitHandler method will post the registration details to the API server by evoking the registerUser function. On successful registration, the React app will redirect the user to the email verification page /verifyemail/:verificationCode. When the request results in an error, an alert notification will be displayed.

src/pages/register.page.tsx


import { object, string, TypeOf } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm, FormProvider, SubmitHandler } from "react-hook-form";
import { useEffect } from "react";
import FormInput from "../components/FormInput";
import { LoadingButton } from "../components/LoadingButton";
import { toast } from "react-toastify";
import { Link, useNavigate } from "react-router-dom";
import { authApi } from "../api/authApi";
import { GenericResponse } from "../api/types";
import useStore from "../store";

const registerSchema = object({
  name: string().min(1, "Full name is required").max(100),
  email: string()
    .min(1, "Email address is required")
    .email("Email Address is invalid"),
  password: string()
    .min(1, "Password is required")
    .min(8, "Password must be more than 8 characters")
    .max(32, "Password must be less than 32 characters"),
  passwordConfirm: string().min(1, "Please confirm your password"),
}).refine((data) => data.password === data.passwordConfirm, {
  path: ["passwordConfirm"],
  message: "Passwords do not match",
});

export type RegisterInput = TypeOf<typeof registerSchema>;

const RegisterPage = () => {
  const store = useStore();
  const navigate = useNavigate();
  const methods = useForm<RegisterInput>({
    resolver: zodResolver(registerSchema),
  });

  const {
    reset,
    handleSubmit,
    formState: { isSubmitSuccessful },
  } = methods;

  useEffect(() => {
    if (isSubmitSuccessful) {
      reset();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isSubmitSuccessful]);

  const registerUser = async (data: RegisterInput) => {
    try {
      store.setRequestLoading(true);
      const response = await authApi.post<GenericResponse>(
        "/auth/register",
        data
      );
      store.setRequestLoading(false);
      toast.success(response.data.message as string, {
        position: "top-right",
      });
      navigate("/verifyemail");
    } catch (error: any) {
      store.setRequestLoading(false);
      const resMessage =
        (error.response &&
          error.response.data &&
          error.response.data.message) ||
        error.message ||
        error.toString();
      toast.error(resMessage, {
        position: "top-right",
      });
    }
  };

  const onSubmitHandler: SubmitHandler<RegisterInput> = (values) => {
    console.log("I was called");
    registerUser(values);
  };
  return (
    <section className="py-8 bg-ct-blue-600 min-h-screen grid place-items-center">
      <div className="w-full">
        <h1 className="text-4xl xl:text-6xl text-center font-[600] text-ct-yellow-600 mb-4">
          Welcome to CodevoWeb!
        </h1>
        <h2 className="text-lg text-center mb-4 text-ct-dark-200">
          Sign Up To Get Started!
        </h2>
        <FormProvider {...methods}>
          <form
            onSubmit={handleSubmit(onSubmitHandler)}
            className="max-w-md w-full mx-auto overflow-hidden shadow-lg bg-ct-dark-200 rounded-2xl p-8 space-y-5"
          >
            <FormInput label="Full Name" name="name" />
            <FormInput label="Email" name="email" type="email" />
            <FormInput label="Password" name="password" type="password" />
            <FormInput
              label="Confirm Password"
              name="passwordConfirm"
              type="password"
            />
            <span className="block">
              Already have an account?{" "}
              <Link to="/login" className="text-ct-blue-600">
                Login Here
              </Link>
            </span>
            <LoadingButton
              loading={store.requestLoading}
              textColor="text-ct-blue-600"
            >
              Sign Up
            </LoadingButton>
          </form>
        </FormProvider>
      </div>
    </section>
  );
};

export default RegisterPage;

The React component template contains the form with input fields and the submit button. Since we created the FormInput component in a different file, we used the FormProvider component provided by React Hook Form to make the form context available to the <FormInput /> components.

Step 6 – Axios GET Request Verify Email

The email verification page contains a form with text input for verifying the user’s email. When the user is redirected to this page from the email sent to their inbox, React will automatically extract the verification code from the URL and enter it into the input field.

src/pages/emailverification.page.tsx


import { object, string, TypeOf } from "zod";
import { useEffect } from "react";
import { useForm, FormProvider, SubmitHandler } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import FormInput from "../components/FormInput";
import { LoadingButton } from "../components/LoadingButton";
import { toast } from "react-toastify";
import { Link, useNavigate, useParams } from "react-router-dom";
import useStore from "../store";
import { authApi } from "../api/authApi";
import { GenericResponse } from "../api/types";

const emailVerificationSchema = object({
  verificationCode: string().min(1, "Email verifciation code is required"),
});

export type EmailVerificationInput = TypeOf<typeof emailVerificationSchema>;

const EmailVerificationPage = () => {
  const store = useStore();
  const navigate = useNavigate();
  const { verificationCode } = useParams();

  const methods = useForm<EmailVerificationInput>({
    resolver: zodResolver(emailVerificationSchema),
  });

  const {
    reset,
    handleSubmit,
    setValue,
    formState: { isSubmitSuccessful },
  } = methods;

  useEffect(() => {
    if (isSubmitSuccessful) {
      reset();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isSubmitSuccessful]);

  useEffect(() => {
    if (verificationCode) {
      setValue("verificationCode", verificationCode);
    }
  }, []);

  const verifyEmail = async (data: EmailVerificationInput) => {
    try {
      store.setRequestLoading(true);
      const response = await authApi.get<GenericResponse>(
        `auth/verifyemail/${data.verificationCode}`
      );
      store.setRequestLoading(false);
      toast.success(response.data.message as string, {
        position: "top-right",
      });
      navigate("/login");
    } catch (error: any) {
      store.setRequestLoading(false);
      const resMessage =
        (error.response &&
          error.response.data &&
          error.response.data.message) ||
        error.message ||
        error.toString();
      toast.error(resMessage, {
        position: "top-right",
      });
    }
  };

  const onSubmitHandler: SubmitHandler<EmailVerificationInput> = (values) => {
    verifyEmail(values);
  };
  return (
    <section className="bg-ct-blue-600 min-h-screen grid place-items-center">
      <div className="w-full">
        <h1 className="text-4xl xl:text-6xl text-center font-[600] text-ct-yellow-600 mb-7">
          Verify Email Address
        </h1>
        <FormProvider {...methods}>
          <form
            onSubmit={handleSubmit(onSubmitHandler)}
            className="max-w-md w-full mx-auto overflow-hidden shadow-lg bg-ct-dark-200 rounded-2xl p-8 space-y-5"
          >
            <FormInput label="Verification Code" name="verificationCode" />
            <LoadingButton
              loading={store.requestLoading}
              textColor="text-ct-blue-600"
            >
              Verify Email
            </LoadingButton>
          </form>
        </FormProvider>
      </div>
    </section>
  );
};

export default EmailVerificationPage;

When the form is submitted, the verifyEmail function will be evoked to submit the verification code to the API server. On successful verification or when the request results in an error, an alert notification will be displayed to the user.

Step 7 – Axios POST Request Login User

The login page contains a form with email and password fields for logging into the React app.

Similar to the account registration form, the form validation is also done with React Hook Form and Zod. When the form is submitted, React Hook Form will validate the form against the Zod schema before calling the loginUser function to post the credentials to the API server.

src/pages/login.page.tsx


import { object, string, TypeOf } from "zod";
import { useEffect } from "react";
import { useForm, FormProvider, SubmitHandler } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import FormInput from "../components/FormInput";
import { LoadingButton } from "../components/LoadingButton";
import { toast } from "react-toastify";
import { Link, useNavigate } from "react-router-dom";
import useStore from "../store";
import { ILoginResponse } from "../api/types";
import { authApi } from "../api/authApi";

const loginSchema = object({
  email: string()
    .min(1, "Email address is required")
    .email("Email Address is invalid"),
  password: string()
    .min(1, "Password is required")
    .min(8, "Password must be more than 8 characters")
    .max(32, "Password must be less than 32 characters"),
});

export type LoginInput = TypeOf<typeof loginSchema>;

const LoginPage = () => {
  const store = useStore();
  const navigate = useNavigate();

  const methods = useForm<LoginInput>({
    resolver: zodResolver(loginSchema),
  });

  const {
    reset,
    handleSubmit,
    formState: { isSubmitSuccessful },
  } = methods;

  useEffect(() => {
    if (isSubmitSuccessful) {
      reset();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isSubmitSuccessful]);

  const loginUser = async (data: LoginInput) => {
    try {
      store.setRequestLoading(true);
      await authApi.post<ILoginResponse>("/auth/login", data);
      store.setRequestLoading(false);
      navigate("/profile");
    } catch (error: any) {
      store.setRequestLoading(false);
      const resMessage =
        (error.response &&
          error.response.data &&
          error.response.data.message) ||
        error.message ||
        error.toString();
      toast.error(resMessage, {
        position: "top-right",
      });
    }
  };

  const onSubmitHandler: SubmitHandler<LoginInput> = (values) => {
    loginUser(values);
  };
  return (
    <section className="bg-ct-blue-600 min-h-screen grid place-items-center">
      <div className="w-full">
        <h1 className="text-4xl xl:text-6xl text-center font-[600] text-ct-yellow-600 mb-4">
          Welcome Back
        </h1>
        <h2 className="text-lg text-center mb-4 text-ct-dark-200">
          Login to have access
        </h2>
        <FormProvider {...methods}>
          <form
            onSubmit={handleSubmit(onSubmitHandler)}
            className="max-w-md w-full mx-auto overflow-hidden shadow-lg bg-ct-dark-200 rounded-2xl p-8 space-y-5"
          >
            <FormInput label="Email" name="email" type="email" />
            <FormInput label="Password" name="password" type="password" />

            <div className="text-right">
              <Link to="#" className="">
                Forgot Password?
              </Link>
            </div>
            <LoadingButton
              loading={store.requestLoading}
              textColor="text-ct-blue-600"
            >
              Login
            </LoadingButton>
            <span className="block">
              Need an account?{" "}
              <Link to="/register" className="text-ct-blue-600">
                Sign Up Here
              </Link>
            </span>
          </form>
        </FormProvider>
      </div>
    </section>
  );
};

export default LoginPage;

On successful authentication, React will redirect the user to the profile page where another request will be made with the access token to retrieve the authenticated user’s information.

Step 8 – Axios GET Request Logout User

The header component will contain the logout button. When the logout button is clicked, a GET request will be made to the /api/auth/logout endpoint on the server to log out the authenticated user. On successful request, the access and refresh token cookies will be automatically deleted in the browser.

After that, React will redirect the user to the login page.

src/components/Header.tsx


import { toast } from "react-toastify";
import { Link } from "react-router-dom";
import useStore from "../store";
import Spinner from "./Spinner";
import { authApi } from "../api/authApi";
import { GenericResponse } from "../api/types";

const Header = () => {
  const store = useStore();
  const user = store.authUser;

  const logoutUser = async () => {
    try {
      store.setRequestLoading(true);
      await authApi.get<GenericResponse>("/auth/logout");
      store.setRequestLoading(false);
      toast.success("Successfully logged out", {
        position: "top-right",
      });
      document.location.href = "/login";
    } catch (error: any) {
      store.setRequestLoading(false);
      store.setAuthUser(null);
      document.location.href = "/login";
    }
  };

  return (
    <>
      <header className="bg-white h-20">
        <nav className="h-full flex justify-between container items-center">
          <div>
            <Link to="/" className="text-ct-dark-600 text-2xl font-semibold">
              CodevoWeb
            </Link>
          </div>
          <ul className="flex items-center gap-4">
            <li>
              <Link to="/" className="text-ct-dark-600">
                Home
              </Link>
            </li>
            {!user && (
              <>
                <li>
                  <Link to="/register" className="text-ct-dark-600">
                    SignUp
                  </Link>
                </li>
                <li>
                  <Link to="/login" className="text-ct-dark-600">
                    Login
                  </Link>
                </li>
              </>
            )}
            {user && (
              <>
                <li>
                  <Link to="/profile" className="text-ct-dark-600">
                    Profile
                  </Link>
                </li>
                <li className="cursor-pointer" onClick={logoutUser}>
                  Logout
                </li>
              </>
            )}
          </ul>
        </nav>
      </header>
      <div className="pt-4 pl-2 bg-ct-blue-600 fixed">
        {store.requestLoading && <Spinner color="text-ct-yellow-600" />}
      </div>
    </>
  );
};

export default Header;

Step 9 – Create Other Pages

To complete the application, let’s create some additional pages. You will create a profile page to demonstrate authentication in React. That means only authenticated users will be allowed to visit the profile page.

Also, you will create a home page to display a welcome message to users.

Create the Profile Page

To implement how authentication works in React, you will create a profile page that the authenticated user will be automatically navigated to after the access and refresh tokens have been received from the backend.

When the user lands on the profile page, a GET request will be made with the access token to the /api/users/me endpoint to retrieve the currently logged-in user’s credentials.

src/pages/profile.page.tsx


import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import { authApi } from "../api/authApi";
import { IUserResponse } from "../api/types";
import useStore from "../store";

const ProfilePage = () => {
  const store = useStore();
  const navigate = useNavigate();

  const getUser = async () => {
    try {
      store.setRequestLoading(true);
      const response = await authApi.get<IUserResponse>("/users/me");
      store.setRequestLoading(false);
      store.setAuthUser(response.data.data.user);
    } catch (error: any) {
      store.setRequestLoading(false);
      const resMessage =
        (error.response &&
          error.response.data &&
          error.response.data.message) ||
        error.message ||
        error.toString();
      toast.error(resMessage, {
        position: "top-right",
      });
      navigate("/login");
    }
  };

  useEffect(() => {
    getUser();
  }, []);

  const user = store.authUser;

  return (
    <section className="bg-ct-blue-600  min-h-screen pt-20">
      <div className="max-w-4xl mx-auto bg-ct-dark-100 rounded-md h-[20rem] flex justify-center items-center">
        <div>
          <p className="text-5xl font-semibold">Profile Page</p>
          <div className="mt-8">
            <p className="mb-4">ID: {user?.id}</p>
            <p className="mb-4">Name: {user?.name}</p>
            <p className="mb-4">Email: {user?.email}</p>
            <p className="mb-4">Role: {user?.role}</p>
          </div>
        </div>
      </div>
    </section>
  );
};

export default ProfilePage;

After the credentials have been received by the React app, we will store them in the Zustand store and display them on the page.

In case there was an error, we will immediately navigate the user to the login page.

Create the Home Page

Here, let’s create a simple React component to display a welcome message to users.

src/pages/home.page.tsx


const HomePage= () => {
  
  return (
  <>
  <section className='bg-ct-blue-600 min-h-screen pt-20'>
        <div className='max-w-4xl mx-auto bg-ct-dark-100 rounded-md h-[20rem] flex justify-center items-center'>
          <p className='text-3xl font-semibold'>Welcome to React.js with Axios</p>
        </div>
  </section>
  </>
  );
};

export default HomePage;

Step 10 – Create a React.js Router for the Pages

In this section, you will utilize the React Router Dom Outlet component to create a layout component that will render the navigation header and a route component.

Create a Layout Component

Instead of adding the <Header /> component to every page that needs it, let’s create a layout component to render any of the defined pages below the Header component depending on the current navigation location.

src/components/Layout.tsx


import { Outlet } from "react-router-dom"
import Header from "./Header"

const Layout = ()=> {
    return <>
    <Header/>
    <Outlet/>
    </>
}

export default Layout

Create the React.js Router

In React Router Dom v6, we are allowed to define the navigation routes in a tree-like structure with objects. So create a src/router/index.tsx file and add the following route definitions.

src/router/index.tsx


import type { RouteObject } from 'react-router-dom';
import Layout from '../components/Layout';
import EmailVerificationPage from '../pages/emailverification.page';
import HomePage from '../pages/home.page';
import LoginPage from '../pages/login.page';
import ProfilePage from '../pages/profile.page';
import RegisterPage from '../pages/register.page';

const authRoutes: RouteObject = {
  path: '*',
  children: [
    {
      path: 'login',
      element: <LoginPage />,
    },
    {
      path: 'register',
      element: <RegisterPage />,
    },
    {
      path: 'verifyemail',
      element: <EmailVerificationPage />,
      children: [
        {
          path: ':verificationCode',
          element: <EmailVerificationPage />,
        },
      ],
    },
  ],
};

const normalRoutes: RouteObject = {
  path: '*',
  element: <Layout />,
  children: [
    {
      index: true,
      element: <HomePage />,
    },
    {
      path: 'profile',
      children: [
        {
          path: '',
          element: <ProfilePage />,
        },
      ],
    },
  ],
};

const routes: RouteObject[] = [authRoutes, normalRoutes];

export default routes;

Add the Router to the Application

Now that we have the routes defined with React Router Dom v6, let’s use the useRoutes() hook to have access to the route object and return it from the App file.

src/App.tsx


import { useRoutes } from "react-router-dom";
import routes from "./router";

function App() {
  const content = useRoutes(routes);
  return content;
}

export default App;

Finally, replace the content of the src/main.tsx file with the following:

src/main.tsx


import "./index.css";
import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
      <ToastContainer />
    </BrowserRouter>
  </React.StrictMode>
);

In the above, we imported the react-toastify CSS and added the <ToastContainer /> component to the code. Next, we wrapped the BrowserRouter component around the App component.

Conclusion

Congrats on reaching the end. In this tutorial, you learned how to implement authentication in React.js using JSON Web Tokens. You also learned how to register users, verify their emails, log them into their accounts, and log them out of their accounts.

You can find the complete source code on this GitHub repository.