How to show confirmation prompt when exiting a page with unsaved changes in a react application

Published on October 9, 2021 • 4m read

Learn to show a native exit prompt, when someone tries to close the window in your react application. Useful for alerting unsaved changes.

Show prompt when exiting a page in a react app for unsaved changes

Image by Walter Knerr from Pixabay.

Let's say you have a react application where you take some input from the user and save it to your server through some API calls. For such use cases, it is a good thing to let the user know that changes might not be saved when they are trying to exit the page. In this blog post, we will see some strategies on implementing such behavior.

TLDR - The Application

Before we see the technique for the exit prompt, let us see, what our dummy app does. It is a very basic application with the following things:

  1. It shows a simple form to the user.
  2. User fills the form and clicks on the SUBMIT button.
  3. The form data is sent to a server through an API call.

The end goal of the application is to show a prompt to the user just before the exit of the page, if

  1. User has entered some value in the form, AND;
  2. The data is not saved to the server yet.

You can see the codesandbox demo above to see it in action. The application is written in TypeScript. Since the app is fairly small, don't worry too much about it. You should understand it just fine and maybe even start liking TypeScript and give it a try yourself (if not already!). Also I have written the same app in plain JavaScript throughout this blog post.

To keep our blog post short and to-the-point

  • We will have only one input in the demo app. The concept should be similar no matter how many form controls you have.
  • The form will not actually be sending the data to the server, rather we will mock it for the demo. We will discuss the code when we come to it.
  • The save to server procedure is always asynchronous, we will observe same behavior in our demo app.

Let's now dive into the different part of the app and see what it does.

Writing the basic application code

Without worrying about the exit prompt, the base code for the application would look something like this.

[jsx]
1import { useEffect, useState } from 'react';
2
3function saveToApi(hobby) {
4 return new Promise(resolve => {
5 setTimeout(() => {
6 console.log(hobby);
7 resolve();
8 }, 1000);
9 });
10}
11
12export default function App() {
13 // our application form state, we could use some libraries here, like Formik
14 // or react hook form
15 const [value, setValue] = useState('');
16
17 return (
18 <div className="App">
19 <form
20 onSubmit={e => {
21 e.preventDefault();
22 // form submit logic goes here
23 // Call our async function to save to the API
24 saveToApi(value).then(() => {
25 // save is done, so we can the value
26 setValue('');
27 });
28 }}
29 >
30 <label>
31 <p>What is your hobby?</p>
32 <input
33 type="text"
34 value={value}
35 onChange={e => {
36 setValue(e.target.value);
37 }}
38 />
39 </label>
40 <p>
41 <button type="submit">SUBMIT</button>
42 </p>
43 </form>
44 </div>
45 );
46}

Copied!

Notice the highlighted saveToApi function. It is a mock implementation of a save to api function. Usually, we would use something like fetch to actually make a call to our API endpoint. But it is not important for the scope of this tutorial.

So what happens is pretty simple. First we have a local state for our form data.

[jsx]
1const [value, setValue] = useState('');

Copied!

Of course, we could've used something like Formik or React Hook Form if the form was more complex.

Then we have a simple form and an onSubmit handler to call to our API.

[jsx]
1<form
2 onSubmit={e => {
3 e.preventDefault();
4 // form submit logic goes here
5 // Call our async function to save to the API
6 saveToApi(value).then(() => {
7 // save is done, so we can the value
8 setValue('');
9 });
10 }}
11>
12 {/** ... */}
13</form>

Copied!

When the form is modified by the user, or is being saved (clicking the SUBMIT button), sudden page exit could be a destructive action. Our goal is to prevent that.

Sample Page Exit Prompt in React App
Exit Prompt in React App

The image above shows the native exit prompt of a browser. We intend to show it when the form is not saved or is being saved.

Observing form state in the application

So from the discussion before, we have already figured out when to show the prompt.

  1. If the form is modified by the user.
  2. If the form is being saved and not finished saving yet.

To track the events, we need a local state in our app. Let's call this formState.

[jsx]
1// A local state where we observe the formState.
2// For our simple app, the three states 'unchaged', 'modified' and 'saving' are
3// sufficient. Modify the logic according to your use-case.
4const [formState, setFormState] = useState('unchanged');

Copied!

There should be three possible values of formState, namely unchanged, modified or saving. This is sufficient for our use-case. If your application is more complex or if you are already observing your form state, then adapt accordingly.

Observing formState in form controls

Now our form begins with unchanged state. It should change to modified when the user enters some value to the input. With this in mind, we modify the onChange handler of the input.

[jsx]
1<input
2 type="text"
3 value={value}
4 onChange={e => {
5 if (e.target.value !== '') {
6 setFormState('modified');
7 } else {
8 setFormState('unchanged');
9 }
10 setValue(e.target.value);
11 }}
12/>

Copied!

If the value is NOT empty, then formState should be modified, else it should stay unchanged.

Observing formState in form submit handler

Now we will need to modify the onSubmit handler of the form to observe the formState.

[jsx]
1<form
2 onSubmit={e => {
3 e.preventDefault();
4 // form submit logic goes here
5 // first set formState to saving
6 setFormState('saving');
7 // Now call our async function to save to the API
8 saveToApi(value).then(() => {
9 // save is done, so we can reset formState and value
10 setValue('');
11 setFormState('unchanged');
12 });
13 }}
14>
15 {/** ... */}
16</form>

Copied!

The saveToApi is an asynchronous operation like most real world use-cases. So before beginning saveToApi, we set formState to saving. Once the save function is finished, we set it to unchanged. In a more real world example, you need to handle error cases too. But for now, let's keep it simple.

Modifying SUBMIT button based on formState

Now that we are properly observing all events for formState, we can change how the SUBMIT button behaves.

[jsx]
1<p>
2 <button type="submit" disabled={formState === 'saving'}>
3 {formState === 'saving' ? 'SUBMITTING' : 'SUBMIT'}
4 </button>
5</p>

Copied!

  • We disable the submit button, when formState is saving. This prevents multiple clicks during save.
  • When form is saving, we show a different text Submitting for a better user experience.

Adding the exit prompt logic

With our formState properly in place, we need to hook into onbeforeunload of window. There are a few things you should note:

  • This event allows us to intercept the exit process of the browser window and present a prompt to the user.
  • We cannot show custom messages or do anything else (like send a fetch request). If the user decides to actually exit the page, by clicking the confirm button, then there's nothing we can do about it.

The intent of this event handler is to show a prompt stating there's unsaved changes in the page. Then it is up to the user whether to exit or come back at the page to save the data.

Again, there's actually no way, to automatically save the data when user decides not to exit the page during the prompt. Your application code needs to take care of presenting an UI for actually saving the data.

The minimal code needed to intercept the exit of a page, looks like this.

[js]
1window.addEventListener('beforeunload', function (e) {
2 // Cancel the event
3 e.preventDefault(); // If you prevent default behavior in Mozilla Firefox prompt will always be shown
4 // Chrome requires returnValue to be set
5 e.returnValue = '';
6});

Copied!

Now, you might've guessed, since this works by adding an event listener to window, we need to make use of react's useEffect hook . The code needed for showing the exit prompt, while observing our previous formState looks like this:

[jsx]
1// A local state where we observe the formState.
2// For our simple app, the three states 'unchaged', 'modified' and 'saving' are
3// sufficient. Modify the logic according to your use-case.
4const [formState, setFormState] = useState('unchanged');
5// The effect where we show an exit prompt, but only if the formState is NOT
6// unchanged. When the form is being saved, or is already modified by the user,
7// sudden page exit could be a destructive action. Our goal is to prevent that.
8useEffect(() => {
9 // the handler for actually showing the prompt
10 // https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload
11 const handler = event => {
12 event.preventDefault();
13 event.returnValue = '';
14 };
15 // if the form is NOT unchanged, then set the onbeforeunload
16 if (formState !== 'unchanged') {
17 window.addEventListener('beforeunload', handler);
18 // clean it up, if the dirty state changes
19 return () => {
20 window.removeEventListener('beforeunload', handler);
21 };
22 }
23 // since this is not dirty, don't do anything
24 return () => {};
25}, [formState]);

Copied!

The logic is pretty simple. In the useEffect if formState is NOT unchanged, then we hook to beforeunload to show the prompt.

If the formState IS unchanged, then we don't. The cleanup functions make sure we remove our event listeners when formState changes or when the component unmounts.

Finished Application with Exit Prompt

With all the modifications, the finished application code will look like this.

[jsx]
1import { useEffect, useState } from "react";
2
3function saveToApi(hobby: string) {
4 return new Promise<void>((resolve) => {
5 setTimeout(() => {
6 console.log(hobby);
7 resolve();
8 }, 1000);
9 });
10}
11
12export default function App() {
13 // our application form state, we could use some libraries here, like Formik
14 // or react hook form
15 const [value, setValue] = useState<string>("");
16
17 // A local state where we observe the formState.
18 // For our simple app, the three states 'unchaged', 'modified' and 'saving' are
19 // sufficient. Modify the logic according to your use-case.
20 const [formState, setFormState] = useState<
21 "unchanged" | "modified" | "saving"
22 >("unchanged");
23
24 // The effect where we show an exit prompt, but only if the formState is NOT
25 // unchanged. When the form is being saved, or is already modified by the user,
26 // sudden page exit could be a destructive action. Our goal is to prevent that.
27 useEffect(() => {
28 // the handler for actually showing the prompt
29 // https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload
30 const handler = (event: BeforeUnloadEvent) => {
31 event.preventDefault();
32 event.returnValue = "";
33 };
34
35 // if the form is NOT unchanged, then set the onbeforeunload
36 if (formState !== "unchanged") {
37 window.addEventListener("beforeunload", handler);
38 // clean it up, if the dirty state changes
39 return () => {
40 window.removeEventListener("beforeunload", handler);
41 };
42 }
43 // since this is not dirty, don't do anything
44 return () => {};
45 }, [formState]);
46 return (
47 <div className="App">
48 <form
49 onSubmit={(e) => {
50 e.preventDefault();
51 // form submit logic goes here
52 // first set formState to saving
53 setFormState("saving");
54 // Now call our async function to save to the API
55 saveToApi(value).then(() => {
56 // save is done, so we can reset formState and value
57 setValue("");
58 setFormState("unchanged");
59 });
60 }}
61 >
62 <label>
63 <p>What is your hobby?</p>
64 <input
65 type="text"
66 value={value}
67 onChange={(e) => {
68 if (e.target.value !== "") {
69 setFormState("modified");
70 } else {
71 setFormState("unchanged");
72 }
73 setValue(e.target.value);
74 }}
75 />
76 </label>
77 <p>
78 <button type="submit" disabled={formState === "saving"}>
79 {formState === "saving" ? "SUBMITTING" : "SUBMIT"}
80 </button>
81 </p>
82 </form>
83 </div>
84 );
85}

Copied!

You can also browse it at codesandbox .


So that was all about having a nice and logical exit prompt for unsaved changes in your react application. I hope it has helped you with your project. Feel free to take the discussion on twitter if you have any doubt or would like to ask me something.

Start building beautiful forms!

Take the next step and get started with WPEForm today. You have the option to start with the free version, or get started with a trial. All your purchases are covered under 30 days Money Back Guarantee.