Putting security measures in place on your React apps to ensure that users register with strong passwords is a top priority for every developer. As users, sometimes we easily forget the passwords we use on websites. Normally, users who create different accounts with unique passwords are more likely to forget their passwords.

If you’ve used Google OAuth to register for accounts on websites before, you may have received an email from Google telling you that “Someone has forcefully logged into your account, please change your password“.

So, it’s crucial to add a forgot/reset password feature to your React app that will allow users to securely reset their passwords when they forget them or when their passwords are found in data breaches.

In this article, you will learn how to implement forgot/reset password feature via email in React using Axios.

Articles in this series:

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

Prerequisites

To follow along with this tutorial, here are some requirements:

  • You should have a basic understanding of JavaScript, TypeScript, and React.js
  • You should have Node.js installed on your machine.
  • Basic knowledge of how JSON Web Token authentication works will be beneficial.

Forgot/Reset Password Workflow in React

The workflow for resetting passwords can be done in different ways depending on the usability and security of your application. This article will implement a standard forgot/reset password workflow. The illustration below will demonstrate how the workflow will work.

The user forgets the password

When a user tries to log in and the server returns an error indicating that either the email or password is invalid, the user can click on the Forgot Password link on the login page.

Also, in the event that the user’s account was compromised, the user can click on the Forgot Password link to start the password reset process.

the user has forgotten his password

Request the password reset link

On the forgot password page, the user will provide the email and click on the send reset code button. Next, React will make an Axios POST request with the email address included in the payload to the /api/auth/forgotpassword endpoint on the server.

The server will then check the database to see if a user with that email exists, generate the password reset HTML template and send the password reset link to the user’s email inbox.

reset password email request in reactjs

From the password reset email, the user can click on the “Reset Password” button to be redirected to the password reset page.

reset password link sent to the users email

Reset the password

On the password reset page, the user will enter the new password and make an Axios PATCH request to the /api/auth/resetpassword/:resetCode endpoint on the server.

On a successful password update, an alert notification will be displayed to the user, and React will redirect the user to the login page.

reset password in reactjs with axios

Run the React Forgot/Reset Password Example Locally

  1. Download or clone the project source code from https://github.com/wpcodevo/reactjs-axios.
  2. Open the project with your IDE and change the Git branch to forgot-reset-password .
  3. Run yarn or yarn install from the terminal in the root directory to install all the dependencies.
  4. Start the Vite development server by running yarn dev .
  5. Open a new tab and go to the application at localhost:3000 .

Step 1 – Run the Backend Server

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

Node.js Forgot/Reset Password Server

For full details about how to create the forgot/reset password functionality with Node.js and Prisma, see the post API with Node.js, Prisma & PostgreSQL: Forget/Reset Password. However, to get the Node.js server up and running quickly so that you can follow along with this tutorial, follow the steps below.

  1. Install Node.js from https://nodejs.org/. Install Yarn with npm install --global yarn .
  2. Download or clone the project source code from https://github.com/wpcodevo/node_prisma_postgresql.
  3. Open the project with your IDE or text editor and change the Git branch to forgot_reset_password .
  4. Run yarn install or yarn to install all the required dependencies.
  5. Duplicate the example.env file and rename the duplicated one to .env
  6. Start the Redis and PostgreSQL Docker containers by running docker-compose up -d
  7. Create the Prisma migration file and push the schema to the PostgreSQL database by running yarn db:migrate && yarn db:push .
  8. Open the .env file and add your SMTP credentials or open the src/app.ts file, uncomment the Nodemailer code, and start the Node.js server with yarn start to generate the Nodemailer SMTP test credentials.
  9. Copy the generated SMTP credentials and add them to the .env file. After that comment out the Nodemailer code again and save the file to restart the server.

Golang Forgot/Reset Password Server

For full details about how to create the Forgot/Reset Password with Golang and MongoDB, see the post Forgot/Reset Passwords in Golang with SMTP HTML Email.

Alternatively, you can follow these steps to get the Golang server up and running quickly.

  1. Install Golang from https://go.dev/doc/install
  2. Download or clone the project source code from https://github.com/wpcodevo/golang-mongodb-api.
  3. Change the Git branch of the project to golang-mongodb-reset-password from the terminal in the project root folder.
  4. Run go mod tidy to install all the packages.
  5. Start the Redis and MongoDB Docker containers with docker-compose up -d .
  6. Copy and paste the example.env file and rename the copied one to app.env
  7. Add your SMTP credentials to the app.env file or register for an account on mailtrap.io and add the SMTP credentials to the app.env file
  8. Start the Golang Gin server with go run main.go .

Step 2 – Create the Forgot Password Page

The forgot password page contains a form built with the React Hook Form library that contains an email field for requesting the password reset link.

The form validation rules are defined with Zod, a popular schema validation library that can be used in both the browser and Node.js. You can see https://github.com/colinhacks/zod for more details.

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

const forgotPasswordchema = object({
  email: string().min(1, "Email is required").email("Invalid email address"),
});

export type ForgotPasswordInput = TypeOf<typeof forgotPasswordchema>;

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

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

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

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

  const forgotPassword = async (data: ForgotPasswordInput) => {
    try {
      store.setRequestLoading(true);
      const response = await authApi.post<GenericResponse>(
        `auth/forgotpassword`,
        data
      );
      store.setRequestLoading(false);
      toast.success(response.data.message as string, {
        position: "top-right",
      });
    } 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<ForgotPasswordInput> = (values) => {
    forgotPassword(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">
          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 Reset Code
            </LoadingButton>
          </form>
        </FormProvider>
      </div>
    </section>
  );
};

export default ForgotPasswordPage;

The onSubmitHandler method will post the user’s email to the API server by evoking the forgotPassword function. On successful submission, the API server will generate the password reset link and send it to the provided email address.

When the request fails, an alert notification will be displayed using the ReactToastify library.

Step 3 – Create the Password Reset Page

The password reset page contains password and password confirmation inputs built with the React Hook Form library and tailwind CSS.

Like the forgot password component, the form validation rules are defined with the Zod schema validation library which React Hook Form supports via the @hookform/resolvers package.

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 { Link, useNavigate, useParams } from "react-router-dom";
import useStore from "../store";
import { authApi } from "../api/authApi";
import { GenericResponse } from "../api/types";

const resetPasswordSchema = object({
  password: string().min(1, "Current password is required"),
  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 {
    reset,
    handleSubmit,
    formState: { isSubmitSuccessful },
  } = methods;

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

  const resetPassword = async (data: ResetPasswordInput) => {
    try {
      store.setRequestLoading(true);
      const response = await authApi.patch<GenericResponse>(
        `auth/resetpassword/${resetCode}`,
        data
      );
      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<ResetPasswordInput> = (values) => {
    if (resetCode) {
      resetPassword(values);
    } else {
      toast.error("Please provide the password reset code", {
        position: "top-right",
      });
    }
  };
  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">
          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 form is submitted, the resetPassword method will be evoked to post the new password to the API server. On a successful password update, an alert notification will be displayed, and React will redirect the user to the login page.

Step 4 – Login with the new Password

The login page contains a form built with the React Hook Form library that contains email and password inputs for signing users into the app.

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="/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;

We created the input field in a different file so we used the FormProvider component provided by React Hook Form to make the form context available to the input components.

Conclusion

Congratulation on reaching the end. In this article, you learned how to implement forgot/reset password functionality via email in a React.js app.

You can find the complete source code of this project in my GitHub repository.