DEV Community

Cover image for How to add the "Changes you made may not be saved" warning to a Next.js app with React Hook Form
Juan Moises Torrijos
Juan Moises Torrijos

Posted on

How to add the "Changes you made may not be saved" warning to a Next.js app with React Hook Form

A modern web application usually has multiple forms. Sometimes these forms can be long and require multiple steps to complete. It can be very frustrating for users when they accidentally navigate away from a form after filling most of it, only to find that all the data they entered has been lost and they have to start over. That's why modern browsers like Google Chrome and Firefox have a warning window that appears when users try to navigate away from an unsaved form. This warning only appears for unsaved forms, which helps users avoid major headaches. However, this feature is only available in browsers for Multi-Page Applications (MPAs), leaving apps built with React, React Router, or Next.js without this functionality.

React Hook Form to the rescue

The React Hook Form library includes an object called formState in the useForm hook. This object has a method called isDirty which returns a boolean value. It keeps track of all the inputs in the form. When any of the inputs change, the boolean value also changes. This makes it convenient to listen for navigation changes and trigger any necessary functions. In our case, we can use it to display an alert to the user.

The first step is to create a hook that we will call useBeforeUnload mimicking the name of the event fired by the browser in a MPA app, like so:

import { useEffect } from 'react';
import { useRouter } from 'next/router';

export default function useBeforeUnload(isDirty) {
  const router = useRouter();
  useEffect(() => {
    const onBeforeUnload = () => {
      if (isDirty) {
        if (
          window.confirm(
            'You have unsaved changes, are you sure you want to leave?'
          )
        ) {
          router.events.emit('routeChangeComplete');
        } else {
          router.events.emit('routeChangeError');
        }
      }
    };
    const handleRouteChangeError = () => {
      throw new Error('Route has been aborted, ignore this message');
    };
    router.events.on('beforeHistoryChange', onBeforeUnload);
    router.events.on('routeChangeError', handleRouteChangeError);
    return () => {
      router.events.off('beforeHistoryChange', onBeforeUnload);
      router.events.off('routeChangeError', handleRouteChangeError);
    };
  }, [isDirty]);
}
Enter fullscreen mode Exit fullscreen mode

The useBeforeUnload hook uses the useEffect hook to access the window object and show a prompt to the user. This is done to mimic the natural behavior of the browser. It's not mandatory, as a custom modal can be used instead. However, using a custom modal is not the best approach because we want to be able to listen for the beforeUnload event on the window. This allows us to display the prompt if the user accidentally closes the window before saving or sending data to the backend. Additionally, we use the useRouter hook to listen for the beforeHistoryChange event. If isDirty is true, we display the prompt to the user. If the user cancels, we emit the routeChangeError. Finally, we throw an error to prevent the router from changing.

We import our custom hook into our contact page like so:

import Head from 'next/head';
import Layout from './components/Layout';
import { useForm } from 'react-hook-form';
import styles from '../styles/Contact.module.css';
import useBeforeUnload from '../hooks/useBeforeUnload.js';

export default function Home() {
  const {
    register,
    handleSubmit,
    formState: { isDirty },
  } = useForm();

  const submitHandler = (data) => {
    console.log(data);
  };

  useBeforeUnload(isDirty);

  return (
    <Layout>
      <Head>
        <title>Contact Page</title>
      </Head>
      <h1>Contact Page</h1>
      <div className="container">
        This is the contact page. Try filling the form below without submitting
        and navigate back to home. A message should appear warning that you have
        unsaved data in the form.
      </div>
      <form
        className={styles.contactForm}
        onSubmit={handleSubmit(submitHandler)}
      >
        <div className={styles.inputWrapper}>
          <label htmlFor="firstname">First Name</label>
          <input type="text" {...register('firstname')} />
        </div>
        <div className={styles.inputWrapper}>
          <label htmlFor="lastname">Last Name</label>
          <input type="text" {...register('lastname')} />
        </div>
        <div className={styles.inputWrapper}>
          <label htmlFor="socialsecurity">Social Security No.</label>
          <input type="text" {...register('socialsecurity')} />
        </div>
        <div className={styles.inputWrapper}>
          <label htmlFor="phone">Phone No.</label>
          <input type="text" {...register('phone')} />
        </div>
        <div className={styles.inputWrapper}>
          <label htmlFor="email">Email</label>
          <input type="email" {...register('name')} />
        </div>
        <div className={styles.inputWrapper}>
          <label htmlFor="message">Say Hello</label>
          <textarea type="text" {...register('message')} />
        </div>
        <button className={styles.submitButton} type="submit">
          Submit
        </button>
      </form>
    </Layout>
  );
}

Enter fullscreen mode Exit fullscreen mode

To test the final result, you can check the Stackblitz project here: Stackblitz project link. Add some data to the input without clicking the submit button, and then navigate to the home page. You will see a warning prompt with a custom message.

To conclude, we should constantly strive to enhance our user's experience by adopting or imitating the existing best practices. A great approach to achieve this is by utilizing the beforeUnload event in our Next.js SPA application. This will prevent our users from facing a significant inconvenience and provide them with a motive to continue using our product.

Top comments (2)

Collapse
 
noitidart profile image
Noitidart

Does router.events.on('beforeHistoryChange', onBeforeUnload) also handle attaching to window.addEventListener("beforeunload", beforeUnloadHandler)?

Collapse
 
juanmtorrijos profile image
Juan Moises Torrijos

This particular tutorial uses the Next.js pages directory, which behaves like a SPA. AFAIK, the window.addEventListener("beforeunload", beforeUnloadHandler) will not fire if you navigate away from a "dirty" form in a SPA.