It's common to come across a situation where a user can navigate away from unsaved changes. For example, a social media site could have a user profile information form. When a user submits the form their data are saved, but if they close the tab before saving, their data are lost. Instead of losing the user's data, it would be nice to show the user a confirmation dialog that warns them of losing unsaved changes when they try to close the tab.
Example use case
To demonstrate, we'll use a simple form that contains an input for the user's name and a button to "save" their name. (In our case, clicking "save" doesn't do anything useful; this is a contrived example.) Here's what that component looks like:
const NameForm = () => {
const [name, setName] = React.useState("");
const [hasUnsavedChanges, setHasUnsavedChanges] = React.useState(undefined);
const handleChange = (event) => {
setName(event.target.value);
setHasUnsavedChanges(true);
};
return (
<div>
<form>
<label htmlFor="name">Your name:</label>
<input
type="text"
id="name"
value={name}
onChange={handleChange}
/>
<button
type="button"
onClick={() => setHasUnsavedChanges(false)}
>
Save changes
</button>
</form>
{typeof hasUnsavedChanges !== "undefined" && (
<div>
You have{" "}
<strong
style={{
color: hasUnsavedChanges
? "firebrick"
: "forestgreen",
}}
>
{hasUnsavedChanges ? "not saved" : "saved"}
</strong>{" "}
your changes.
</div>
)}
</div>
);
}
And here is the form in use:
If the user closes the tab without saving their name first, we want to show a confirmation dialog that looks similar to this:
Custom hook solution
We'll create a hook named useConfirmTabClose
that will show the dialog if the user tries to close the tab when hasUnsavedChanges
is true
. We can use it in our component like this:
const NameForm = () => {
const [name, setName] = React.useState("");
const [hasUnsavedChanges, setHasUnsavedChanges] = React.useState(undefined);
useConfirmTabClose(hasUnsavedChanges);
// ...
}
We can read this hook as "confirm the user wants to close the tab if they have unsaved changes."
Showing the confirmation dialog
To implement this hook, we need to know when the user has closed the tab and show the dialog. We can add an event listener for the beforeunload
event to detect when the window, the document, and the document's resources are about to be unloaded (see References for more information about this event).
The event handler that we provide can tell the browser to show the confirmation dialog. The way this is implemented varies by browser, but I've found success on Chrome and Safari by assigning a non-empty string to event.returnValue
and also by returning a string. For example:
const confirmationMessage = "You have unsaved changes. Continue?";
const handleBeforeUnload = (event) => {
event.returnValue = confirmationMessage;
return confirmationMessage;
}
window.addEventListener("beforeunload", handleBeforeUnload);
Note: The string returned or assigned to event.returnValue
may not be shown in the confirmation dialog as that feature is deprecated and not widely supported. Also, the way that we indicate that the dialog should be opened is not consistently implemented across browsers. According to MDN, the spec states that the event handler should call event.preventDefault()
to show the dialog, though Chrome and Safari don't seem to respect this.
Hook implementation
Now that we know how to show the confirmation dialog, let's start creating the hook. We'll take one argument, isUnsafeTabClose
, which is some boolean value that should tell us if we should show the confirmation dialog. We'll also add the beforeunload
event listener in an useEffect
hook and ensure that we remove the event listener once the component has unmounted:
const confirmationMessage = "You have unsaved changes. Continue?";
const useConfirmTabClose = (isUnsafeTabClose) => {
React.useEffect(() => {
const handleBeforeUnload = (event) => {};
window.addEventListener("beforeunload", handleBeforeUnload);
return () =>
window.removeEventListener("beforeunload", handleBeforeUnload);
}, [isUnsafeTabClose]);
};
We know that we can assign event.returnValue
or return a string from the beforeunload
handler to show the confirmation dialog, so in handleBeforeUnload
we can simply do that if isUnsafeTabClose
is true
:
const confirmationMessage = "You have unsaved changes. Continue?";
const useConfirmTabClose = (isUnsafeTabClose) => {
React.useEffect(() => {
const handleBeforeUnload = (event) => {
if (isUnsafeTabClose) {
event.returnValue = confirmationMessage;
return confirmationMessage;
}
}
// ...
}
Putting those together, we have the final version of our hook:
const confirmationMessage = "You have unsaved changes. Continue?";
const useConfirmTabClose = (isUnsafeTabClose) => {
React.useEffect(() => {
const handleBeforeUnload = (event) => {
if (isUnsafeTabClose) {
event.returnValue = confirmationMessage;
return confirmationMessage;
}
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () =>
window.removeEventListener("beforeunload", handleBeforeUnload);
}, [isUnsafeTabClose]);
};
Final component
Here is the final version of NameForm
after adding our custom hook:
const NameForm = () => {
const [name, setName] = React.useState("");
const [hasUnsavedChanges, setHasUnsavedChanges] = React.useState(undefined);
useConfirmTabClose(hasUnsavedChanges);
const handleChange = (event) => {
setName(event.target.value);
setHasUnsavedChanges(true);
};
return (
<div>
<form>
<label htmlFor="name">Your name:</label>
<input
type="text"
id="name"
value={name}
onChange={handleChange}
/>
<button
type="button"
onClick={() => setHasUnsavedChanges(false)}
>
Save changes
</button>
</form>
{typeof hasUnsavedChanges !== "undefined" && (
<div>
You have{" "}
<strong
style={{
color: hasUnsavedChanges
? "firebrick"
: "forestgreen",
}}
>
{hasUnsavedChanges ? "not saved" : "saved"}
</strong>{" "}
your changes.
</div>
)}
</div>
);
}
Conclusion
In this post, we used the beforeunload
event to alert the user when closing a tab with unsaved changes. We created useConfirmTabClose
, a custom hook that adds and removes the beforeunload
event handler and checks if we should show a confirmation dialog or not.
References
Cover photo by Jessica Tan on Unsplash
Let's connect
If you liked this post, come connect with me on Twitter, LinkedIn, and GitHub! You can also subscribe to my mailing list and get the latest content and news from me.
Top comments (4)
Unfortunately this is terrible advice. You should never stop the user from leaving the page, this is one of the most annoying things you that you can do to them. Additionally most browsers don't even let you do this:
Lastly, the event while supported is totally unreliable, the user may leave the page because of any number of reasons:
You'll never get the event in these cases. The only right thing to do is to always persist the state to localstorage, and in some cases to your service for syncing purposes. You can store drafts of important changes automatically. There's no reason you should ever stop the user from leaving your page.
Warren, I disagree with your assertion about this being terrible advice and that you should "never" stop them from leaving the page. If you have a long form, or any user entered data, and the user intends to save it, but does not for any reason, putting it in localStorage doesn't solve the problem. The user has no idea they didn't save or submit. Taking steps to help them realize what happening, is helpful.
Yes, there's lots of places this is abused and it's irritating, but any form that takes time to fill out, or even small forms with lots of text, benefit from checking with the user to try to help them avoid data loss/wasted time.
Thanks for the article Zach!
Why create one when you can get all awesome hooks in a single library?
Try scriptkavi/hooks. Copy paste style and easy to integrate with its own CLI
Can we use custom modal instead of window.confirm()?
Thanks for the article Zach!