This article will give you a comprehensive overview of JSON Web Token authentication with React Query. You will learn how to validate forms with React Hook Form, make queries and mutations with React Query, register new users, verify their email addresses, sign the users into the React app, and log them out of the app.
Related articles in this series:
- React Query and Axios: User Registration and Email Verification
- Forgot/Reset Passwords with React Query and Axios
More practice:
- How to Setup tRPC API Server & Client with Next.js and Prisma
- tRPC Server API with Next.js & PostgreSQL: Access & Refresh Tokens
- Full-Stack Next.js tRPC App: User Registration & Login Example
- Build a tRPC CRUD API Example with Next.js
Prerequisites
To benefit the most from this tutorial, you should have:
- Basic understanding of JavaScript and TypeScript
- Basic understanding of React.js
- Basic knowledge of CSS and tailwind CSS
- The latest version of Node.js installed on your system
- Basic knowledge of CRUD (Create, Read, Update, and Delete) and API design.
- Optional: Install the latest version of Docker on your system. The Docker installation is needed to run the API servers provided in this article.
React Query and Axios User Authentication Flow
The React Query JWT authentication app will have the following routes:
/login
– a public route for signing users into their accounts./register
– a public route where a user can create a new account./verifyemail/:verificationCode
– a public route for verifying the registered user’s email address./profile
– a private route where the currently authenticated user’s credentials will be displayed./
– a public route where a simple welcome message will be displayed.
Register User
To register for an account, provide the required credentials and click the signup button. On submission, React will make a mutation request to the /api/auth/register
endpoint with React Query and Axios.
On successful registration, the API will send a verification link to the user’s email inbox and return a success message to the React app. At this point, the user will be required to verify the provided email address before being allowed to log in.
Verify Email Address
Below is a sample of the email sent to the user’s email inbox. When the Verify Your Account button is clicked, the user will be automatically redirected to the React email verification page.
When the React email verification component mounts, the verification code will be automatically extracted from the URL and entered into the input field.
On submission, a mutation request will be made to the /api/auth/verifyemail/:verificationCode
API endpoint with React Query.
Sign in User
After verifying the email address, you can now provide the email and password to sign into the React app. On submission, a mutation request will be made with React Query to the /api/auth/login
endpoint on the API server.
The API server will validate the credentials, check the database to see if the email exists, validate the password against the hashed one in the database, generate access and refresh tokens, and return the tokens with an HTTPOnly directive to the React app or client.
The access and refresh tokens are sent as cookies to the browser or client. This will make it difficult for hackers to access and manipulate them. The React app is configured in such a way that it will automatically include the cookies in all subsequent requests made to the API.
Retrieve the User’s Profile
On successful authentication, React will redirect the user to the profile page where a query request will be made with React Query to the /api/users/me
endpoint to retrieve the currently logged-in user’s credentials.
Step 1 – Setup and Run the Backend Server
To test the React app with a functioning API, follow the steps below to set up and run the API server with either Node.js or Golang. In an upcoming article, I will include APIs built with Python and ASP.NET.
Backend server running on Node.js
For full details on how to create the Node.js API with Express and PostgreSQL, see the post API with Node.js + PostgreSQL + TypeORM: Send Emails. Nevertheless, you can follow the steps below to get Node.js API up and running quickly.
- Clone or download the API source code from https://github.com/wpcodevo/node_typeorm.
- Change the Git branch to
jwt_auth_verify_email
- From the terminal, run
yarn install
oryarn
to install all the required dependencies. - Start the PostgreSQL and Redis servers by running the command
docker-compose up -d
in the terminal. - Duplicate the example.env file and rename the duplicated file to .env.
- Add your SMTP credentials to the .env file. Alternatively, you can generate the SMTP credentials with Nodemailer. To do that, uncomment the Nodemailer code in the
src/app.ts
file and start the Node.js server with yarn start to generate the SMTP test credentials in the terminal. - Add the SMTP credentials to the .env file. After that comment out the Nodemailer code again and save the
src/app.ts
file to restart the API server.
Backend server running on Golang
For full details on how to create the API with Golang, MongoDB, and Gin Gonic, see the post API with Golang + MongoDB: Send HTML Emails with Gomail. Nevertheless, you can follow the steps below to quickly get the Golang API up and running.
- Download or clone the Golang API source code from https://github.com/wpcodevo/golang-mongodb-api.
- Open the project with your text editor or IDE and change the Git branch to
golang-mognodb-send-emails
. - Open the integrated terminal and run
go mod tidy
to install all the required packages. - Start the MongoDB server by running
docker-compose up -d
. - Copy and paste the example.env file and rename the duplicated file to app.env.
- Add your SMTP credentials to the app.env file. Alternatively, create a mailtrap.io account and add the SMTP credentials provided by mailtrap.io to the app.env file.
- Start the Golang Gin Gonic API by running
go run main.go
from the command line in the project root directory.
Step 2 – Setup React with Vite and TailwindCSS
We are now ready to bootstrap the React project. Instead of using Create React App, we will use the Vite scaffolding tool to generate the React project.
To do that, run the following command to create the React.js boilerplate project with Vite. You can name the project react-query-axios
.
yarn create vite react-query-axios --template react-ts
# or
npm create vite@latest react-query-axios -- --template react-ts
The above command will generate a React app that uses TypeScript. Change the directory into the generated project and install all the dependencies.
cd react-query-axios && yarn
# or
cd react-query-axios && npm install
Optional: Open the package.json file and change the dev script to "dev": "vite --host localhost --port 3000"
. This will tell Vite to start the development server on port 3000 instead of 5173.
With that out of the way, let’s install Tailwind CSS and its peer dependencies:
yarn add -D tailwindcss postcss autoprefixer
# or
npm install -D tailwindcss postcss autoprefixer
With the dependencies installed, run the following command to generate the tailwind.config.cjs
and postcss.config.cjs
files:
yarn tailwindcss init -p
# or
npx tailwindcss init -p
Next, add the following Tailwind CSS configurations to the tailwind.config.cjs
:
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: [],
};
Now add the tailwind CSS directives to src/index.css
file:
src/index.css
@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, start the Vite development server by running yarn dev
or npm run dev
.
Step 3 – Setup Axios and State Management
In this section, you will create all the Axios API requests that React Query will use to make the HTTP requests to the backend API. Apart from that, you will set up Zustand, a library that makes state management in React a breeze.
Create the API Response Types
To begin, create a src\api\types.ts
file and add the following TypeScript interfaces that will be used to 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 the Axios API Requests
Axios is a promise-based HTTP client that can be used in the browser and node.js. Even though Axios is popularly known for making HTTP requests, It can be configured to make GraphQL requests.
Install the Axios package with the following command:
yarn add axios
# or
npm install axios
With Axios installed, create a src/api/authApi.ts
file and add the following code:
src/api/authApi.ts
import axios from 'axios';
import { LoginInput } from '../pages/login.page';
import { RegisterInput } from '../pages/register.page';
import { GenericResponse, ILoginResponse, IUserResponse } from './types';
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';
export const signUpUserFn = async (user: RegisterInput) => {
const response = await authApi.post<GenericResponse>('auth/register', user);
return response.data;
};
export const loginUserFn = async (user: LoginInput) => {
const response = await authApi.post<ILoginResponse>('auth/login', user);
return response.data;
};
export const verifyEmailFn = async (verificationCode: string) => {
const response = await authApi.get<GenericResponse>(
`auth/verifyemail/${verificationCode}`
);
return response.data;
};
export const logoutUserFn = async () => {
const response = await authApi.get<GenericResponse>('auth/logout');
return response.data;
};
export const getMeFn = async () => {
const response = await authApi.get<IUserResponse>('users/me');
return response.data;
};
Don’t worry about the red squiggly lines, they will varnish when we create the login and register pages. In the above, we created an Axios instance and five functions that will be provided to React Query.
signUpUserFn
– This will make an Axios POST request to register the user.loginUserFn
– This will make an Axios POST request to sign in the user.verifyEmailFn
– This will make an Axios GET request to verify the user’s email address.logoutUserFn
– This will make an Axios GET request to log out the user.getMeFn
– This will make an Axios GET request to retrieve the authenticated user’s information.
Setup Zustand
Zustand is a new state manager in React. It uses flux principles to make state management in React a breeze. Apart from that, it can also be used in other JavaScript frameworks like Svelte, Vue, Angular, and even Vanilla JS.
Though Redux is known to be the most popular state management library in React, Zustand uses a different approach to provide a simpler interface for managing states in React.
As we all know, Redux is robust and requires a lot of setup code to work. With Redux you need to create actions, dispatch functions, and reducers before you can manage states in React. Zustand on the other hand provides simple hooks that can be used to manage states.
Install Zustand with this command:
yarn add zustand
# or
npm install zustand
To manage states with Zustand, you need to create a store that will contain all the application states and functions to update the states.
To do that, 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;
Store
– defines the TypeScript types for the Zustand store.useStore
– a hook that will be used to access and mutate the store.
Step 4 – Create Reusable Components
In this step, you will create reusable components with tailwind CSS and React. To begin install the following dependencies:
yarn add zod react-hook-form @hookform/resolvers tailwind-merge react-toastify react-router-dom@6.3.0
tailwind-merge
– This library provides a utility function to efficiently merge Tailwind CSS classes in JS.react-hook-form
– A React.js form validation library.zod
– A typescript-first schema validation library that can be used in the Browser and Node.js.react-toastify
– A library for displaying notification alerts.
Input Field Component
The first component will be used to capture input data in a form. To create this component, we will use React Hook Form to provide the validations and tailwind CSS for the styling.
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;
Since the FormInput component is located in a different file, we used the useFormContext()
hook to provide the form context to it. This way we can delegate the form control to React Hook Form.
Loading Spinner
Let’s create a spinner component that will be shown when a request is being processed.
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 button that has a spinner and text with tailwind CSS and React. To do that, create a src/components/LoadingButton.tsx
file and add the code below.
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>
);
};
Message Component
Here, create a src/components/Message.tsx
file and add the following code snippets to create a Message component that will be used to display messages.
src/components/Message.tsx
import { FC } from "react";
import { Link } from "react-router-dom";
interface IMessage {
children: React.ReactNode;
}
const Message: FC<IMessage> = ({ children }) => {
return (
<div className="max-w-4xl mx-auto bg-ct-dark-100 rounded-md h-[20rem] w-[35rem] py-12 px-8 flex flex-col items-center text-center">
<h3 className="text-4xl font-semibold mb-8">Almost there!</h3>
{children}
</div>
);
};
export default Message;
Step 5 – React Query Request: Register User
In this section, you will create the React component that will make the mutation request with React Query to the /api/auth/register
endpoint.
To begin, install these dependencies:
yarn add @tanstack/react-query @tanstack/react-query-devtools
react-query
– A server state management library for Reactreact-query-devtools
– A GUI for managing React Query state
The account registering page contains a form component built with React and styled with tailwind CSS. The form will contain fields like name, email, password, and password confirmation.
Since the FormInput component is located in a different file, we will use the FormProvider component provided by React Hook Form to make the form context available to the FormInput.
The form validation rules are defined within a schema using the Zod schema validation library and provided to the useForm hook via @hookform/resolvers
package.
src/pages/register.page.tsx
import { object, string, TypeOf, z } 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 } from "react-router-dom";
import { signUpUserFn } from "../api/authApi";
import useStore from "../store";
import { useMutation } from "@tanstack/react-query";
import Message from "../components/Message";
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 methods = useForm<RegisterInput>({
resolver: zodResolver(registerSchema),
});
// ? Calling the Register Mutation
const {
mutate: registerUser,
data,
isSuccess,
} = useMutation((userData: RegisterInput) => signUpUserFn(userData), {
onMutate(variables) {
store.setRequestLoading(true);
},
onSuccess(data) {
store.setRequestLoading(false);
toast.success(data?.message);
},
onError(error: any) {
store.setRequestLoading(false);
if (Array.isArray((error as any).response.data.error)) {
(error as any).response.data.error.forEach((el: any) =>
toast.error(el.message, {
position: "top-right",
})
);
} else {
toast.error((error as any).response.data.message, {
position: "top-right",
});
}
},
});
const {
reset,
handleSubmit,
formState: { isSubmitSuccessful },
} = methods;
useEffect(() => {
if (isSubmitSuccessful) {
reset();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSubmitSuccessful]);
const onSubmitHandler: SubmitHandler<RegisterInput> = (values) => {
// ? Execute the Mutation
registerUser(values);
};
return (
<section className="py-8 bg-ct-blue-600 min-h-screen grid place-items-center">
{data && isSuccess ? (
<Message>
<p className="text-xl">{data.message}</p>
<p className="mt-8">
Already confirmed? Then you can{" "}
<Link to="/login" className="text-blue-700 underline">
Log in
</Link>
</p>
</Message>
) : (
<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;
When the onSubmitHandler
function is evoked, the registerUser
method will be called to make a mutation request with the credentials obtained from the form to the /api/auth/register
endpoint.
Once the mutation resolves successfully, the Message component will be rendered to display the message sent by the API.
Step 6 – React Query Request: Verify Email
The email verification page contains a form that has the FormInput component we defined above. Similar to the registration page, the form validation will also be done with React Hook Form and Zod.
When the user is redirected to this page, React will use the useParams()
hook to extract the verification code from the URL and enter it into the FormInput component.
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, verifyEmailFn } from "../api/authApi";
import { GenericResponse } from "../api/types";
import { useMutation } from "@tanstack/react-query";
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 { mutate: verifyEmail } = useMutation(
(verificationCode: string) => verifyEmailFn(verificationCode),
{
onSuccess: (data) => {
toast.success(data?.message);
navigate("/login");
},
onError(error: any) {
if (Array.isArray((error as any).data.error)) {
(error as any).data.error.forEach((el: any) =>
toast.error(el.message, {
position: "top-right",
})
);
} else {
toast.error((error as any).data.message, {
position: "top-right",
});
}
},
}
);
const {
reset,
handleSubmit,
formState: { isSubmitSuccessful },
} = methods;
useEffect(() => {
if (isSubmitSuccessful) {
reset();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSubmitSuccessful]);
useEffect(() => {
if (verificationCode) {
reset({ verificationCode });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onSubmitHandler: SubmitHandler<EmailVerificationInput> = ({
verificationCode,
}) => {
// ? Executing the verifyEmail Mutation
verifyEmail(verificationCode);
};
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 onSubmitHandler function is evoked, the verifyEmail method will be called to make a React Query mutation request to the /api/auth/verifyemail/:verificationCode
endpoint on the API.
After the mutation resolves successfully, React will redirect the user to the login page and an alert notification will be displayed.
Step 7 – React Query Request: Login User
In this step, you will create the React component to log the user into the application. This component will fire a mutation request with React Query and Axios to the /api/auth/login
endpoint.
The account login page contains a form component that has email and password fields. The form validation is handled by React Hook Form and the validation rules are defined in a schema with Zod.
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, useLocation, useNavigate } from "react-router-dom";
import useStore from "../store";
import { ILoginResponse } from "../api/types";
import { authApi, getMeFn, loginUserFn } from "../api/authApi";
import { useQuery, useMutation } from "@tanstack/react-query";
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 navigate = useNavigate();
const location = useLocation();
const from = ((location.state as any)?.from.pathname as string) || "/profile";
const methods = useForm<LoginInput>({
resolver: zodResolver(loginSchema),
});
const store = useStore();
// API Login Mutation
const { mutate: loginUser } = useMutation(
(userData: LoginInput) => loginUserFn(userData),
{
onMutate(variables) {
store.setRequestLoading(true);
},
onSuccess: () => {
store.setRequestLoading(false);
toast.success("You successfully logged in");
navigate(from);
},
onError: (error: any) => {
store.setRequestLoading(false);
if (Array.isArray((error as any).response.data.error)) {
(error as any).response.data.error.forEach((el: any) =>
toast.error(el.message, {
position: "top-right",
})
);
} else {
toast.error((error as any).response.data.message, {
position: "top-right",
});
}
},
}
);
const {
reset,
handleSubmit,
formState: { isSubmitSuccessful },
} = methods;
useEffect(() => {
if (isSubmitSuccessful) {
reset();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSubmitSuccessful]);
const onSubmitHandler: SubmitHandler<LoginInput> = (values) => {
// ? Executing the loginUser Mutation
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="/forgotpassword" 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;
Once the onSubmitHandler function is evoked by React Hook Form, the loginUser()
mutation method will be called to make a mutation request with the JSON object included in the request body to the /api/auth/login
endpoint.
On successful mutation, React will redirect the user to the /profile
page where a query request will be fired with the access token included in the request headers to retrieve the user’s information.
When the mutation request resolves in errors, an alert notification will be displayed to list the errors sent from the API.
Step 8 – React Query Request: Logout User
In this step, you will create a header navigation component that will have a logout button. When the logout button is clicked, React will fire a mutation with React Query to the /api/auth/logout
endpoint.
On success, the API will return expired cookies to clear the existing ones in the browser or client. Apart from that, React will redirect the user to the /login
page.
When the mutation results in errors, React will display an alert notification to list the errors returned from the API.
src/components/Header.tsx
import { toast } from "react-toastify";
import { Link } from "react-router-dom";
import useStore from "../store";
import Spinner from "./Spinner";
import { logoutUserFn } from "../api/authApi";
import { useMutation } from "@tanstack/react-query";
const Header = () => {
const store = useStore();
const user = store.authUser;
const { mutate: logoutUser } = useMutation(() => logoutUserFn(), {
onMutate(variables) {
store.setRequestLoading(true);
},
onSuccess(data) {
store.setRequestLoading(false);
toast.success("Successfully logged out", {
position: "top-right",
});
document.location.href = "/login";
},
onError(error: any) {
store.setRequestLoading(false);
store.setAuthUser(null);
document.location.href = "/login";
},
});
const handleLogout = () => {
logoutUser();
};
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={handleLogout}>
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 with TailwindCSS
To make the authentication flow complete, you will create a profile page that can only be accessed when the user has been authenticated by the backend API. After the user has been logged into the React app, the cookies sent by the API will be included in all subsequent requests to access public and private routes on the API.
Profile Page
On successful authentication, React will redirect the user to the profile page. When the profile page component mounts, a query request will be fired with React Query to retrieve the authenticated user’s information.
Since the /api/users/me
endpoint is protected, React will send the access token cookie along with the request.
src/pages/profile.page.tsx
import { useQuery } from "@tanstack/react-query";
import { toast } from "react-toastify";
import { getMeFn } from "../api/authApi";
import useStore from "../store";
const ProfilePage = () => {
const store = useStore();
const { data } = useQuery(["getMe"], getMeFn, {
select(data) {
return data.data.user;
},
onSuccess(data) {
store.setAuthUser(data);
store.setRequestLoading(false);
},
onError(error) {
store.setRequestLoading(false);
if (Array.isArray((error as any).response.data.error)) {
(error as any).response.data.error.forEach((el: any) =>
toast.error(el.message, {
position: "top-right",
})
);
} else {
toast.error((error as any).response.data.message, {
position: "top-right",
});
}
},
});
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 profile information has been received by the React app, the store.setAuthUser() method will be evoked to store the data in the Zustand store.
In the event that the request results in an error, an alert notification will be displayed and the user will be redirected to the login page.
Home Page
Here, create a src/pages/home.page.tsx
file and add the following code. This React component will 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 – Setup Routing in React
In this section, you will create routes for all the pages we defined above, use the Outlet component to create a layout component, and provide the React Query client to the entry point file.
To begin, create a src/components/Layout.tsx
file and add the following code. Having a layout component will prevent us from adding the Header component to every page that needs it.
The Outlet component is simply a placeholder where child route elements will be rendered.
src/components/Layout.tsx
import { Outlet } from "react-router-dom"
import Header from "./Header"
const Layout = ()=> {
return <>
<Header/>
<Outlet/>
</>
}
export default Layout
To define the routes for the React components, create a src/router/index.tsx
file and add the following. In React Router Dom v6, we can now create the navigation routes with objects instead of using Route Elements.
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;
In the src/App.tsx
file, evoke the useRoutes()
hook and provide it with the routes we defined above. After that, return the route object returned by the useRoutes()
hook from the App
component.
The useRoutes hook will return the element of the route that matches the current location.
src/App.tsx
import { useRoutes } from "react-router-dom";
import routes from "./router";
function App() {
const content = useRoutes(routes);
return content;
}
export default App;
It’s now time to finalize everything. Open the src/main.tsx
file and replace the content 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 App from "./App";
import { BrowserRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 1000,
},
},
});
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<App />
<ToastContainer />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</BrowserRouter>
</React.StrictMode>
);
In the above, we wrapped the QueryClientProvider and BrowserRouter components around the App component. The QueryClientProvider element will connect and provide the QueryClient to the application.
Conclusion
In this article, you learned how to implement JSON Web Token authentication with React Query, Axios, React Hook Form, and Zod. Also, you learned how to:
- Register users with React
- Sign in the registered user
- Log out the authenticated user
- Verify the user’s email address
You can find the React Query project source code from this GitHub repository.