How to add the "Changes you made may not be saved" warning to a Next.js app with React Hook Form
Tags: webdev, javascript, nextjs, react
Published: 2023-08-17 01:00:31

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:
```js
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]);
}
```

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:
```jsx
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>
  );
}

```
To test the final result, you can check the Stackblitz project here: [Stackblitz project link](https://stackblitz.com/edit/nextjs-aqnzms?file=pages%2F_app.js). 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.