Using a strong and unique password on the internet is a crucial requirement to secure our accounts. The majority of users who use unique passwords are more likely to forget their passwords. For this reason, it makes a lot of sense to add forgot/reset password feature to our Web, Mobile, and Desktop applications.

Accounting for a forgot/reset password feature will make it easy for users to reset their passwords when the passwords are found in data breaches.

Forgot-Reset Passwords with React Q...
Forgot-Reset Passwords with React Query and Axios

This article will teach you how to implement a forgot/reset password functionality with React, Axios, React Query, Zustand, and TypeScript.

Related articles in this series:

  1. React Query and Axios: User Registration and Email Verification
  2. Forgot/Reset Passwords with React Query and Axios

More Practice:

Forgot-Reset Passwords with React Query and Axios

Prerequisites

To get the best out of this tutorial, you should have:

  • Basic understanding of JavaScript and TypeScript
  • Basic knowledge of React and React Query
  • Basic understanding of CSS and Tailwind CSS
  • Basic understanding of API design and how to interact with APIs
  • Basic understanding of JWT authentication

Forgot/Reset Passwords Flow with React Query

This article will teach you how to forget/reset passwords with React Query. Below are the React routes defined in this project:

  • /login – a public route for authenticating users by signing them into their accounts
  • /register – a public route for creating new users
  • /verifyemail/:verificationCode – a public route for verifying a user’s email.
  • /profile – a private route that displays the authenticated user’s credentials.
  • /forgotpassword – a public route for getting the password reset link from the API
  • /resetpassword/:resetCode – a public route for resetting the passwords
  • / – a public route that displays a simple welcome message

Failed Login

Upon several failed login attempts, the user can click on the Forgot Password link available on the login page to start the forgot/reset password process.

react query and axios forgot reset password failed login

Get Password Reset Link

On the forgot password page, the user will provide the email address and click on the Send Password Reset Link button. React will fire a React Query mutation request with Axios to the /api/auth/forgotpassword endpoint.

The API will:

  1. Validate the request body with Zod
  2. Query the database to check if a user with that email exists
  3. Generate the HTML template and password reset code
  4. Send the password reset link to the user’s email inbox
react query and axios request password reset link

After React receives a response from the API, an alert notification will be shown and a message component will be rendered to display the success message sent by the server.

react query and axios request password reset link success

Reset Password

Below is a sample of the Email HTML template sent to the user’s email inbox. Once the user clicks the Reset Password button, the browser will redirect the user to the /resetpassword/:resetCode page.

react query and axios forgot reset password email

On the password reset route, the user will be required to input the new password and confirm it. When the user clicks the Reset Password button, React will make a mutation request with React Query to the /api/auth/resetpassword/:resetCode endpoint.

On success, React will automatically redirect the user to the login page and an alert notification will be displayed.

react query and axios forgot reset password mutation request

Login with New Passwords

On the login page, the user will provide the email address and the new password. Upon clicking the Login button, React will make a React Query mutation request to the /api/auth/login endpoint with the JSON object included in the request body.

On success, React will log the user into the app.

react query and axios forgot reset password login mutation request

Run the Forgot/Reset Passwords with React Query Locally

  1. Get the project source code from https://github.com/wpcodevo/react-query-axios-tailwindcss
  2. Open the project with a text editor and change the Git branch to react-query-tailwindcss-axios-reset-password.
  3. Install all the dependencies by running yarn or yarn install from the terminal in the root directory.
  4. Run yarn dev to start the Vite development server.
  5. Open a new tab in the browser and go to the application at localhost:3000.

Run the Backend API

Below are two APIs built with Node.js and Golang:

Forgot/Reset Passwords Backend Built with Golang

Read the post Forgot/Reset Passwords in Golang with SMTP HTML Email to get a full guide on how to build the forgot/reset password API with Golang.

Nevertheless, you can use the following steps to get the Golang API up and running:

  1. Download and install the latest version of Golang from https://go.dev/doc/install
  2. Download or clone the forgot/reset password API source code from https://github.com/wpcodevo/golang-mongodb-api.
  3. Open the project with an IDE and change the Git branch of the source code to golang-mongodb-reset-password.
  4. Install all the dependencies by running go mod tidy from the terminal in the project root directory.
  5. Start the MongoDB database server by running docker-compose up -d.
  6. Make a copy of the example.env file and rename the duplicated one to app.env
  7. Open the app.env file and add your SMTP credentials. Alternatively, create an account on mailtrap.io, add a new inbox, click the settings icon under Action on the newly-created inbox, copy the SMTP credentials, and add them to the app.env file.
  8. Run go run main.go to start the Golang Gin server.

Forgot/Reset Passwords Backend Built with Node.js

Read the post API with Node.js, Prisma & PostgreSQL: Forget/Reset Password to get full detail on how to create the forgot/reset password functionality with Node.js.

However, you can use the following steps to quickly set up and run the Node.js API.

  1. Download and install Node.js from https://nodejs.org/. Runnpm install --global yarn to install Yarn globally.
  2. Get the forgot/reset password API source code from https://github.com/wpcodevo/node_prisma_postgresql.
  3. Open the source code with your IDE or text editor and change the Git branch to forgot_reset_password.
  4. Install all the required dependencies by running yarn install or yarn
  5. Make a copy of example.env file and rename the copied file to .env
  6. Start the Redis and PostgreSQL servers by running docker-compose up -d in the terminal of the root project folder.
  7. Run yarn db:migrate && yarn db:push to create the database migration file, generate the Prisma Client, and push the database schema to the PostgreSQL database.
  8. Open the .env file and add your SMTP credentials. Alternatively, open the src/app.ts file, uncomment the Nodemailer code, and run yarn start to start the Node.js server. This will cause Nodemailer to generate SMTP test account credentials in the terminal.
  9. Copy the SMTP credentials generated in the terminal and add them to the .env file. Once you are done, comment out the Nodemailer code in the src/app.ts file again and save the file to restart the Node.js server.

Step 1 – Create the API Requests with Axios

In this section, you will create a custom Axios instance and add some default configurations. Also, you will create Axios request functions that React Query will use behind the scene to make the mutation and query requests.

Out of the box, Axios comes configured with some default configurations, and since we want to customize the Axios instance with our own configurations, we will use the Axios.create() method and pass an object containing the parameters as an argument.

Using a custom Axios instance will help us reuse the provided configurations for all the API invocations made by the instance.

src/api/authApi.ts


import axios from 'axios';
import { LoginInput } from '../pages/login.page';
import { RegisterInput } from '../pages/register.page';
import { ResetPasswordInput } from '../pages/resetpassword.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;
};

export const forgotPasswordFn = async (email: string) => {
  const response = await authApi.post<GenericResponse>('auth/forgotpassword',{email});
  return response.data;
};

export const resetPasswordFn = async (data: ResetPasswordInput, resetCode: string) => {
  const response = await authApi.patch<GenericResponse>(`auth/resetpassword/${resetCode}`, data);
  return response.data;
};

Quite a lot of Axios functions, let’s evaluate them:

  • signUpUserFn – This function will be used by React Query to register the new user by making a mutation request to the API.
  • loginUserFn – This function will be used by React Query to authenticate the user by making a mutation request to the backend API.
  • verifyEmailFn – This function will be evoked by React Query to verify the user’s email address.
  • logoutUserFn – This function will be evoked by React Query to log out the user.
  • getMeFn – This function will be evoked by React Query to retrieve the authenticated user’s information from the API.
  • forgotPasswordFn – This function will be evoked by React Query to request the password reset link from the API.
  • resetPasswordFn – This function will be used by React Query to reset the user’s password by firing a mutation request to the API.

Step 2 – Create the Forgot Password Component

In this step, you will create the React component that will fire a React Query mutation request with Axios to the /api/auth/forgotpassword endpoint.

This component will contain a form with email input and a button to submit the data. The validation schema will be defined with Zod and the form validation will be done with React-Hook-Form.

src/pages/forgotpassword.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 useStore from "../store";
import { forgotPasswordFn } from "../api/authApi";
import { useMutation } from "@tanstack/react-query";
import Message from "../components/Message";
import { Link } from "react-router-dom";

const forgotPasswordSchema = object({
  email: string()
    .min(1, "Email verifciation code is required")
    .email("Email address is invalid"),
});

export type ForgotPasswordInput = TypeOf<typeof forgotPasswordSchema>;

const ForgotPasswordPage = () => {
  const store = useStore();

  const methods = useForm<ForgotPasswordInput>({
    resolver: zodResolver(forgotPasswordSchema),
  });

  const {
    mutate: forgotPassword,
    data,
    isSuccess,
  } = useMutation((email: string) => forgotPasswordFn(email), {
    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).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]);

  const onSubmitHandler: SubmitHandler<ForgotPasswordInput> = ({ email }) => {
    // ? Executing the forgotPassword Mutation
    forgotPassword(email);
  };

  return (
    <section className="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">
            Didn't forget password{" "}
            <Link to="/login" className="text-blue-700 underline">
              Go back to the login
            </Link>
          </p>
        </Message>
      ) : (
        <div className="w-full">
          <h1 className="text-4xl lg:text-6xl text-center font-[600] text-ct-yellow-600 mb-14">
            Forgot Password
          </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="Email Address" name="email" type="email" />
              <LoadingButton
                loading={store.requestLoading}
                textColor="text-ct-blue-600"
              >
                Send Password Reset Link
              </LoadingButton>
            </form>
          </FormProvider>
        </div>
      )}
    </section>
  );
};

export default ForgotPasswordPage;

You will notice we used React Query’s useMutation hook and passed the forgotPasswordFn function we created with Axios as the mutation function. When the mutate method which we renamed to forgotPassword is evoked, React Query will fire a mutation request to the /api/auth/forgotpassword endpoint on the API.

The API will then generate the password reset code and the HTML email template and send the password reset link to the user’s email inbox. On success, React will render the Message component to display the response sent by the API.

Step 3 – Create the Password Reset Component

Now that we are able to request the password reset email, let’s create the React component for resetting the password. This component will have a form with password and password confirmation inputs.

Because the password reset code is included in the URL, we will use React-Router-Dom’s useParams() hook to extract it from the URL. The password reset code will be required by the API to verify the user’s identity before the password data can be updated in the database.

src/pages/resetpassword.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 { useNavigate, useParams } from "react-router-dom";
import useStore from "../store";
import { resetPasswordFn } from "../api/authApi";
import { useMutation } from "@tanstack/react-query";

const resetPasswordSchema = object({
  password: string()
    .min(1, "Password is required")
    .min(8, "Password must be at least 8 characters"),
  passwordConfirm: string().min(1, "Please confirm your password"),
});

export type ResetPasswordInput = TypeOf<typeof resetPasswordSchema>;

const ResetPasswordPage = () => {
  const store = useStore();
  const navigate = useNavigate();
  const { resetCode } = useParams();

  const methods = useForm<ResetPasswordInput>({
    resolver: zodResolver(resetPasswordSchema),
  });

  const { mutate: resetPassword } = useMutation(
    (data: ResetPasswordInput) => resetPasswordFn(data, resetCode!),
    {
      onMutate(variables) {
        store.setRequestLoading(true);
      },
      onSuccess: (data) => {
        store.setRequestLoading(false);
        toast.success(data?.message);
        navigate("/login");
      },
      onError(error: any) {
        store.setRequestLoading(false);
        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]);

  const onSubmitHandler: SubmitHandler<ResetPasswordInput> = (values) => {
    resetPassword(values);
  };

  return (
    <section className="bg-ct-blue-600 min-h-screen grid place-items-center">
      <div className="w-full">
        <h1 className="text-4xl lg:text-6xl text-center font-[600] text-ct-yellow-600 mb-14">
          Reset Password
        </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="New Password" name="password" type="password" />
            <FormInput
              label="Confirm Password"
              name="passwordConfirm"
              type="password"
            />
            <LoadingButton
              loading={store.requestLoading}
              textColor="text-ct-blue-600"
            >
              Reset Password
            </LoadingButton>
          </form>
        </FormProvider>
      </div>
    </section>
  );
};

export default ResetPasswordPage;

When the resetPassword mutation method is evoked, React Query will fire a mutation request to submit the form data and the reset code to the API. On a successful password update, the user will be redirected to the login page to sign in with the new password.

Step 4 – Create the Login Component

The final step in the password reset process is to require the user to log in with the new password. This React component will contain a form with email and password inputs.

The form validation is done via React-Hook-Form with the rules defined in the Zod schema.

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 { loginUserFn } from "../api/authApi";
import { 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;

Conclusion

Congrats on reaching the end. In this article, you learned how to add a forgot/reset password feature to a React.js application. Also, you learned how to make mutations and queries with React Query.

You can find the React Query forgot/reset password source code on this GitHub repository https://github.com/wpcodevo/react-query-axios-tailwindcss/tree/react-query-tailwindcss-axios-reset-password.