DEV Community

Cover image for Creating Custom React Hooks: useConfirmTabClose
Zach Snoek
Zach Snoek

Posted on • Updated on

Creating Custom React Hooks: useConfirmTabClose

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

And here is the form in use:

ezgif.com-video-to-gif

If the user closes the tab without saving their name first, we want to show a confirmation dialog that looks similar to this:

Screen Shot 2021-03-29 at 7.57.17 PM

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);

    // ...
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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]);
};
Enter fullscreen mode Exit fullscreen mode

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;
            }
        }
        // ...
}
Enter fullscreen mode Exit fullscreen mode

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]);
};
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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 (3)

Collapse
 
wparad profile image
Warren Parad • Edited

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:

Event disabled

Lastly, the event while supported is totally unreliable, the user may leave the page because of any number of reasons:

  • They want to
  • They shut down their browser
  • They are locking it or turning it off.

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.

Collapse
 
jedgrant profile image
Jed Grant

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!

Collapse
 
hoanghuynh0609 profile image
Hoang Huynh

Can we use custom modal instead of window.confirm()?
Thanks for the article Zach!