Zach Snoek
Zach Snoek's Blog

Zach Snoek's Blog

Creating Custom React Hooks: useConfirmTabClose

Creating Custom React Hooks: useConfirmTabClose

Zach Snoek's photo
Zach Snoek
·Mar 30, 2021·

5 min read

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:

fdxv4ywjintsv4947mbu.gif

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

bhh73zx5d5hidrw96yo4.jpeg

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!

 
Share this